Commit 6629d5d3 by cahrens

Merge branch 'master' into talbs/studio-authorship

Conflicts:
	cms/djangoapps/contentstore/views/user.py
	cms/static/sass/elements/_controls.scss
	cms/templates/activation_active.html
	cms/templates/activation_complete.html
	cms/templates/activation_invalid.html
parents e1c02b1b 9a61038c
...@@ -45,3 +45,4 @@ node_modules ...@@ -45,3 +45,4 @@ node_modules
autodeploy.properties autodeploy.properties
.ws_migrations_complete .ws_migrations_complete
.vagrant/ .vagrant/
logs
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
[edx-studio.django-partial] [edx-platform.django-partial]
file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po
source_file = conf/locale/en/LC_MESSAGES/django-partial.po source_file = conf/locale/en/LC_MESSAGES/django-partial.po
source_lang = en source_lang = en
type = PO type = PO
[edx-studio.djangojs] [edx-platform.djangojs]
file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = conf/locale/en/LC_MESSAGES/djangojs.po source_file = conf/locale/en/LC_MESSAGES/djangojs.po
source_lang = en source_lang = en
type = PO type = PO
[edx-studio.mako] [edx-platform.mako]
file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po
source_file = conf/locale/en/LC_MESSAGES/mako.po source_file = conf/locale/en/LC_MESSAGES/mako.po
source_lang = en source_lang = en
type = PO type = PO
[edx-studio.messages] [edx-platform.messages]
file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po
source_file = conf/locale/en/LC_MESSAGES/messages.po source_file = conf/locale/en/LC_MESSAGES/messages.po
source_lang = en source_lang = en
......
...@@ -81,3 +81,4 @@ Felix Sun <felixsun@mit.edu> ...@@ -81,3 +81,4 @@ Felix Sun <felixsun@mit.edu>
Adam Palay <adam@edx.org> Adam Palay <adam@edx.org>
Ian Hoover <ihoover@edx.org> Ian Hoover <ihoover@edx.org>
Mukul Goyal <miki@edx.org> Mukul Goyal <miki@edx.org>
Robert Marks <rmarks@edx.org>
...@@ -5,10 +5,13 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,10 +5,13 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Common: Added *experimental* support for jsinput type. Common: Added *experimental* support for jsinput type.
Common: Added setting to specify Celery Broker vhost Common: Added setting to specify Celery Broker vhost
Common: Utilize new XBlock bulk save API in LMS and CMS.
Studio: Add table for tracking course creator permissions (not yet used). Studio: Add table for tracking course creator permissions (not yet used).
Update rake django-admin[syncdb] and rake django-admin[migrate] so they Update rake django-admin[syncdb] and rake django-admin[migrate] so they
run for both LMS and CMS. run for both LMS and CMS.
...@@ -21,6 +24,8 @@ Studio: Added support for uploading and managing PDF textbooks ...@@ -21,6 +24,8 @@ Studio: Added support for uploading and managing PDF textbooks
Common: Student information is now passed to the tracking log via POST instead of GET. Common: Student information is now passed to the tracking log via POST instead of GET.
Blades: Added functionality and tests for new capa input type: choicetextresponse.
Common: Add tests for documentation generation to test suite Common: Add tests for documentation generation to test suite
Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems
...@@ -43,6 +48,13 @@ history of background tasks for a given problem and student. ...@@ -43,6 +48,13 @@ history of background tasks for a given problem and student.
Blades: Small UX fix on capa multiple-choice problems. Make labels only Blades: Small UX fix on capa multiple-choice problems. Make labels only
as wide as the text to reduce accidental choice selections. as wide as the text to reduce accidental choice selections.
Studio:
- use xblock field defaults to initialize all new instances' fields and
only use templates as override samples.
- create new instances via in memory create_xmodule and related methods rather
than cloning a db record.
- have an explicit method for making a draft copy as distinct from making a new module.
Studio: Remove XML from the video component editor. All settings are Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata. moved to be edited as metadata.
......
...@@ -239,7 +239,6 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run: ...@@ -239,7 +239,6 @@ CMS templates. Fortunately, `rake` will do all of this for you! Just run:
$ rake django-admin[syncdb] $ rake django-admin[syncdb]
$ rake django-admin[migrate] $ rake django-admin[migrate]
$ rake cms:update_templates
If you are running these commands using the [`zsh`](http://www.zsh.org/) shell, If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
zsh will assume that you are doing zsh will assume that you are doing
......
...@@ -20,8 +20,8 @@ def get_course_updates(location): ...@@ -20,8 +20,8 @@ def get_course_updates(location):
try: try:
course_updates = modulestore('direct').get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"]) modulestore('direct').create_and_save_xmodule(location)
course_updates = modulestore('direct').clone_item(template, Location(location)) course_updates = modulestore('direct').get_item(location)
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored} # current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
location_base = course_updates.location.url() location_base = course_updates.location.url()
......
...@@ -10,6 +10,7 @@ Feature: Course checklists ...@@ -10,6 +10,7 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page And They are correctly selected after I reload the page
@skip
Scenario: A task can link to a location within Studio Scenario: A task can link to a location within Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to the course outline When I select a link to the course outline
......
...@@ -208,8 +208,9 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): ...@@ -208,8 +208,9 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
def i_created_a_video_component(step): def i_created_a_video_component(step):
world.create_component_instance( world.create_component_instance(
step, '.large-video-icon', step, '.large-video-icon',
'i4x://edx/templates/video/default', 'video',
'.xmodule_VideoModule' '.xmodule_VideoModule',
has_multiple_templates=False
) )
......
...@@ -7,10 +7,16 @@ from terrain.steps import reload_the_page ...@@ -7,10 +7,16 @@ from terrain.steps import reload_the_page
@world.absorb @world.absorb
def create_component_instance(step, component_button_css, instance_id, expected_css): def create_component_instance(step, component_button_css, category,
expected_css, boilerplate=None,
has_multiple_templates=True):
click_new_component_button(step, component_button_css) click_new_component_button(step, component_button_css)
click_component_from_menu(instance_id, expected_css)
if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css)
assert_equal(1, len(world.css_find(expected_css)))
@world.absorb @world.absorb
def click_new_component_button(step, component_button_css): def click_new_component_button(step, component_button_css):
...@@ -19,7 +25,7 @@ def click_new_component_button(step, component_button_css): ...@@ -19,7 +25,7 @@ def click_new_component_button(step, component_button_css):
@world.absorb @world.absorb
def click_component_from_menu(instance_id, expected_css): def click_component_from_menu(category, boilerplate, expected_css):
""" """
Creates a component from `instance_id`. For components with more Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new than one template, clicks on `elem_css` to create the new
...@@ -27,12 +33,13 @@ def click_component_from_menu(instance_id, expected_css): ...@@ -27,12 +33,13 @@ def click_component_from_menu(instance_id, expected_css):
as the user clicks the appropriate button, so we assert that the as the user clicks the appropriate button, so we assert that the
expected component is present. expected component is present.
""" """
elem_css = "a[data-location='%s']" % instance_id if boilerplate:
elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
else:
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css) elements = world.css_find(elem_css)
assert(len(elements) == 1) assert_equal(len(elements), 1)
if elements[0]['id'] == instance_id: # If this is a component with multiple templates world.css_click(elem_css)
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
@world.absorb @world.absorb
......
...@@ -8,8 +8,9 @@ from lettuce import world, step ...@@ -8,8 +8,9 @@ from lettuce import world, step
def i_created_discussion_tag(step): def i_created_discussion_tag(step):
world.create_component_instance( world.create_component_instance(
step, '.large-discussion-icon', step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag', 'discussion',
'.xmodule_DiscussionModule' '.xmodule_DiscussionModule',
has_multiple_templates=False
) )
...@@ -17,14 +18,14 @@ def i_created_discussion_tag(step): ...@@ -17,14 +18,14 @@ def i_created_discussion_tag(step):
def i_see_only_the_settings_and_values(step): def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries( world.verify_all_setting_entries(
[ [
['Category', "Week 1", True], ['Category', "Week 1", False],
['Display Name', "Discussion Tag", True], ['Display Name', "Discussion Tag", False],
['Subcategory', "Topic-Level Student-Visible Label", True] ['Subcategory', "Topic-Level Student-Visible Label", False]
]) ])
@step('creating a discussion takes a single click') @step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step): def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule')) assert(not world.is_css_present('.xmodule_DiscussionModule'))
world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']") world.css_click("a[data-category='discussion']")
assert(world.is_css_present('.xmodule_DiscussionModule')) assert(world.is_css_present('.xmodule_DiscussionModule'))
...@@ -7,11 +7,11 @@ from lettuce import world, step ...@@ -7,11 +7,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$') @step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step): def i_created_blank_html_page(step):
world.create_component_instance( world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page', step, '.large-html-icon', 'html',
'.xmodule_HtmlModule' '.xmodule_HtmlModule'
) )
@step('I see only the HTML display name setting$') @step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step): def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]]) world.verify_all_setting_entries([['Display Name', "Blank HTML Page", False]])
...@@ -18,8 +18,9 @@ def i_created_blank_common_problem(step): ...@@ -18,8 +18,9 @@ def i_created_blank_common_problem(step):
world.create_component_instance( world.create_component_instance(
step, step,
'.large-problem-icon', '.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem', 'problem',
'.xmodule_CapaModule' '.xmodule_CapaModule',
'blank_common.yaml'
) )
...@@ -35,8 +36,8 @@ def i_see_five_settings_with_values(step): ...@@ -35,8 +36,8 @@ def i_see_five_settings_with_values(step):
[DISPLAY_NAME, "Blank Common Problem", True], [DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False], [MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False], [PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", True], [RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", True] [SHOW_ANSWER, "Finished", False]
]) ])
...@@ -94,7 +95,7 @@ def my_change_to_randomization_is_persisted(step): ...@@ -94,7 +95,7 @@ def my_change_to_randomization_is_persisted(step):
def i_can_revert_to_default_for_randomization(step): def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION) world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step) world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False) world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Never", False)
@step('I can set the weight to "(.*)"?') @step('I can set the weight to "(.*)"?')
...@@ -156,7 +157,7 @@ def create_latex_problem(step): ...@@ -156,7 +157,7 @@ def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon') world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab. # Go to advanced tab.
world.css_click('#ui-id-2') world.css_click('#ui-id-2')
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule') world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
@step('I edit and compile the High Level Source') @step('I edit and compile the High Level Source')
...@@ -169,7 +170,8 @@ def edit_latex_source(step): ...@@ -169,7 +170,8 @@ def edit_latex_source(step):
@step('my change to the High Level Source is persisted') @step('my change to the High Level Source is persisted')
def high_level_source_persisted(step): def high_level_source_persisted(step):
def verify_text(driver): def verify_text(driver):
return world.css_text('.problem') == 'hi' css_sel = '.problem div>span'
return world.css_text(css_sel) == 'hi'
world.wait_for(verify_text) world.wait_for(verify_text)
...@@ -203,7 +205,7 @@ def verify_modified_display_name_with_special_chars(): ...@@ -203,7 +205,7 @@ def verify_modified_display_name_with_special_chars():
def verify_unset_display_name(): def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False) world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False)
def set_weight(weight): def set_weight(weight):
......
...@@ -22,7 +22,7 @@ def have_a_course_with_1_section(step): ...@@ -22,7 +22,7 @@ def have_a_course_with_1_section(step):
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template='i4x://edx/templates/sequential/Empty', category='sequential',
display_name='Subsection One',) display_name='Subsection One',)
...@@ -33,18 +33,18 @@ def have_a_course_with_two_sections(step): ...@@ -33,18 +33,18 @@ def have_a_course_with_two_sections(step):
section = world.ItemFactory.create(parent_location=course.location) section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create( subsection1 = world.ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template='i4x://edx/templates/sequential/Empty', category='sequential',
display_name='Subsection One',) display_name='Subsection One',)
section2 = world.ItemFactory.create( section2 = world.ItemFactory.create(
parent_location=course.location, parent_location=course.location,
display_name='Section Two',) display_name='Section Two',)
subsection2 = world.ItemFactory.create( subsection2 = world.ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty', category='sequential',
display_name='Subsection Alpha',) display_name='Subsection Alpha',)
subsection3 = world.ItemFactory.create( subsection3 = world.ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template='i4x://edx/templates/sequential/Empty', category='sequential',
display_name='Subsection Beta',) display_name='Subsection Beta',)
......
...@@ -7,7 +7,7 @@ from lettuce import world, step ...@@ -7,7 +7,7 @@ from lettuce import world, step
@step('I see the correct settings and default values$') @step('I see the correct settings and default values$')
def i_see_the_correct_settings_and_values(step): def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False], world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'default', True], ['Display Name', 'Video Title', False],
['Download Track', '', False], ['Download Track', '', False],
['Download Video', '', False], ['Download Video', '', False],
['Show Captions', 'True', False], ['Show Captions', 'True', False],
......
...@@ -14,7 +14,7 @@ def does_not_autoplay(_step): ...@@ -14,7 +14,7 @@ def does_not_autoplay(_step):
@step('creating a video takes a single click') @step('creating a video takes a single click')
def video_takes_a_single_click(_step): def video_takes_a_single_click(_step):
assert(not world.is_css_present('.xmodule_VideoModule')) assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']") world.css_click("a[data-category='video']")
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
......
from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from json import dumps
from xmodule.modulestore.inheritance import own_metadata
from django.conf import settings
filter_list = ['xml_attributes', 'checklists']
class Command(BaseCommand):
help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized
in a JSON format. This can be used for analytics.'''
def handle(self, *args, **options):
if len(args) < 2 or len(args) > 3:
raise CommandError("dump_course_structure requires two or more arguments: <location> <outfile> |<db>|")
course_id = args[0]
outfile = args[1]
# use a user-specified database name, if present
# this is useful for doing dumps from databases restored from prod backups
if len(args) == 3:
settings.MODULESTORE['direct']['OPTIONS']['db'] = args[2]
loc = CourseDescriptor.id_to_location(course_id)
store = modulestore()
course = None
try:
course = store.get_item(loc, depth=4)
except:
print 'Could not find course at {0}'.format(course_id)
return
info = {}
def dump_into_dict(module, info):
filtered_metadata = dict((key, value) for key, value in own_metadata(module).iteritems()
if key not in filter_list)
info[module.location.url()] = {
'category': module.location.category,
'children': module.children if hasattr(module, 'children') else [],
'metadata': filtered_metadata
}
for child in module.get_children():
dump_into_dict(child, info)
dump_into_dict(course, info)
with open(outfile, 'w') as f:
f.write(dumps(info))
...@@ -14,11 +14,11 @@ unnamed_modules = 0 ...@@ -14,11 +14,11 @@ unnamed_modules = 0
class Command(BaseCommand): class Command(BaseCommand):
help = 'Import the specified data directory into the default ModuleStore' help = 'Export the specified data directory into the default ModuleStore'
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 2: if len(args) != 2:
raise CommandError("import requires two arguments: <course location> <output path>") raise CommandError("export requires two arguments: <course location> <output path>")
course_id = args[0] course_id = args[0]
output_path = args[1] output_path = args[1]
...@@ -30,4 +30,4 @@ class Command(BaseCommand): ...@@ -30,4 +30,4 @@ class Command(BaseCommand):
root_dir = os.path.dirname(output_path) root_dir = os.path.dirname(output_path)
course_dir = os.path.splitext(os.path.basename(output_path))[0] course_dir = os.path.splitext(os.path.basename(output_path))[0]
export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir) export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir, modulestore())
###
### Script for exporting all courseware from Mongo to a directory
###
import os
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
unnamed_modules = 0
class Command(BaseCommand):
help = 'Export all courses from mongo to the specified data directory'
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("export requires one argument: <output path>")
output_path = args[0]
cs = contentstore()
ms = modulestore('direct')
root_dir = output_path
courses = ms.get_courses()
print "%d courses to export:" % len(courses)
cids = [x.id for x in courses]
print cids
for course_id in cids:
print "-"*77
print "Exporting course id = {0} to {1}".format(course_id, output_path)
if 1:
try:
location = CourseDescriptor.id_to_location(course_id)
course_dir = course_id.replace('/', '...')
export_to_xml(ms, cs, location, root_dir, course_dir, modulestore())
except Exception as err:
print "="*30 + "> Oops, failed to export %s" % course_id
print "Error:"
print err
from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
update_templates(modulestore('direct'))
...@@ -3,13 +3,13 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -3,13 +3,13 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
def get_module_info(store, location, parent_location=None, rewrite_static_links=False): def get_module_info(store, location, rewrite_static_links=False):
try: try:
module = store.get_item(location) module = store.get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
# create a new one # create a new one
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) store.create_and_save_xmodule(location)
module = store.clone_item(template_location, location) module = store.get_item(location)
data = module.data data = module.data
if rewrite_static_links: if rewrite_static_links:
...@@ -29,7 +29,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -29,7 +29,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
'id': module.location.url(), 'id': module.location.url(),
'data': data, 'data': data,
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
'metadata': module._model_data._kvs._metadata # what's the intent here? all metadata incl inherited & namespaced?
'metadata': module.xblock_kvs._metadata
} }
...@@ -37,14 +38,11 @@ def set_module_info(store, location, post_data): ...@@ -37,14 +38,11 @@ def set_module_info(store, location, post_data):
module = None module = None
try: try:
module = store.get_item(location) module = store.get_item(location)
except: except ItemNotFoundError:
pass # new module at this location: almost always used for the course about pages; thus, no parent. (there
# are quite a handful of about page types available for a course and only the overview is pre-created)
if module is None: store.create_and_save_xmodule(location)
# new module at this location module = store.get_item(location)
# presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
if post_data.get('data') is not None: if post_data.get('data') is not None:
data = post_data['data'] data = post_data['data']
...@@ -79,4 +77,4 @@ def set_module_info(store, location, post_data): ...@@ -79,4 +77,4 @@ def set_module_info(store, location, post_data):
# commit to datastore # commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(location, module._model_data._kvs._metadata) store.update_metadata(location, module.xblock_kvs._metadata)
...@@ -46,6 +46,8 @@ class ChecklistTestCase(CourseTestCase): ...@@ -46,6 +46,8 @@ class ChecklistTestCase(CourseTestCase):
# Now delete the checklists from the course and verify they get repopulated (for courses # Now delete the checklists from the course and verify they get repopulated (for courses
# created before checklists were introduced). # created before checklists were introduced).
self.course.checklists = None self.course.checklists = None
# Save the changed `checklists` to the underlying KeyValueStore before updating the modulestore
self.course.save()
modulestore = get_modulestore(self.course.location) modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course)) modulestore.update_metadata(self.course.location, own_metadata(self.course))
self.assertEqual(self.get_persisted_checklists(), None) self.assertEqual(self.get_persisted_checklists(), None)
......
...@@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date from xmodule.fields import Date
from .utils import CourseTestCase from .utils import CourseTestCase
...@@ -36,7 +37,6 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -36,7 +37,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end)) self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus)) self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
...@@ -49,7 +49,6 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -49,7 +49,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized") self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized")
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
self.assertIsNone(jsondetails['effort'], "effort somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
...@@ -291,6 +290,71 @@ class CourseGradingTest(CourseTestCase): ...@@ -291,6 +290,71 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2") self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
def test_update_cutoffs_from_json(self):
test_grader = CourseGradingModel.fetch(self.course.location)
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs)
# Unlike other tests, need to actually perform a db fetch for this test since update_cutoffs_from_json
# simply returns the cutoffs you send into it, rather than returning the db contents.
altered_grader = CourseGradingModel.fetch(self.course.location)
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update")
test_grader.grade_cutoffs['D'] = 0.3
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs)
altered_grader = CourseGradingModel.fetch(self.course.location)
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D")
test_grader.grade_cutoffs['Pass'] = 0.75
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs)
altered_grader = CourseGradingModel.fetch(self.course.location)
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'")
def test_delete_grace_period(self):
test_grader = CourseGradingModel.fetch(self.course.location)
CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period)
# update_grace_period_from_json doesn't return anything, so query the db for its contents.
altered_grader = CourseGradingModel.fetch(self.course.location)
self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")
test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period)
altered_grader = CourseGradingModel.fetch(self.course.location)
self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period")
test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0}
# Now delete the grace period
CourseGradingModel.delete_grace_period(test_grader.course_location)
# update_grace_period_from_json doesn't return anything, so query the db for its contents.
altered_grader = CourseGradingModel.fetch(self.course.location)
# Once deleted, the grace period should simply be None
self.assertEqual(None, altered_grader.grace_period, "Delete grace period")
def test_update_section_grader_type(self):
# Get the descriptor and the section_grader_type and assert they are the default values
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format)
self.assertEqual(False, descriptor.lms.graded)
# Change the default grader type to Homework, which should also mark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'})
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Homework', section_grader_type['graderType'])
self.assertEqual('Homework', descriptor.lms.format)
self.assertEqual(True, descriptor.lms.graded)
# Change the grader type back to Not Graded, which should also unmark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'})
descriptor = get_modulestore(self.course.location).get_item(self.course.location)
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format)
self.assertEqual(False, descriptor.lms.graded)
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
""" """
...@@ -352,7 +416,7 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -352,7 +416,7 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness # check for deletion effectiveness
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in') self.assertEqual('finished', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in') self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
......
...@@ -36,8 +36,11 @@ class CourseUpdateTest(CourseTestCase): ...@@ -36,8 +36,11 @@ class CourseUpdateTest(CourseTestCase):
'provided_id': payload['id']}) 'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>' content += '<div>div <p>p<br/></p></div>'
payload['content'] = content payload['content'] = content
# POST requests were coming in w/ these header values causing an error; so, repro error here
resp = self.client.post(first_update_url, json.dumps(payload), resp = self.client.post(first_update_url, json.dumps(payload),
"application/json") "application/json",
HTTP_X_HTTP_METHOD_OVERRIDE="PUT",
REQUEST_METHOD="POST")
self.assertHTMLEqual(content, json.loads(resp.content)['content'], self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div") "iframe w/ div")
......
...@@ -35,7 +35,6 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -35,7 +35,6 @@ class InternationalizationTest(ModuleStoreTestCase):
self.user.save() self.user.save()
self.course_data = { self.course_data = {
'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx', 'org': 'MITx',
'number': '999', 'number': '999',
'display_name': 'Robot Super Course', 'display_name': 'Robot Super Course',
......
from contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.capa_module import CapaDescriptor
import json
from xmodule.modulestore.django import modulestore
import datetime
from pytz import UTC
class DeleteItem(CourseTestCase): class DeleteItem(CourseTestCase):
...@@ -11,14 +16,228 @@ class DeleteItem(CourseTestCase): ...@@ -11,14 +16,228 @@ class DeleteItem(CourseTestCase):
def testDeleteStaticPage(self): def testDeleteStaticPage(self):
# Add static tab # Add static tab
data = { data = json.dumps({
'parent_location': 'i4x://mitX/333/course/Dummy_Course', 'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'template': 'i4x://edx/templates/static_tab/Empty' 'category': 'static_tab'
} })
resp = self.client.post(reverse('clone_item'), data) resp = self.client.post(reverse('create_item'), data,
content_type="application/json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore). # Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json") resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
class TestCreateItem(CourseTestCase):
"""
Test the create_item handler thoroughly
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
def test_create_nicely(self):
"""
Try the straightforward use cases
"""
# create a chapter
display_name = 'Nicely created'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
# get the new item and check its category and display_name
chap_location = self.response_id(resp)
new_obj = modulestore().get_item(chap_location)
self.assertEqual(new_obj.category, 'chapter')
self.assertEqual(new_obj.display_name, display_name)
self.assertEqual(new_obj.location.org, self.course.location.org)
self.assertEqual(new_obj.location.course, self.course.location.course)
# get the course and ensure it now points to this one
course = modulestore().get_item(self.course.location)
self.assertIn(chap_location, course.children)
# use default display name
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': chap_location,
'category': 'vertical'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
vert_location = self.response_id(resp)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({
'parent_location': vert_location,
'category': 'problem',
'boilerplate': template_id
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
prob_location = self.response_id(resp)
problem = modulestore('draft').get_item(prob_location)
# ensure it's draft
self.assertTrue(problem.is_draft)
# check against the template
template = CapaDescriptor.get_template(template_id)
self.assertEqual(problem.data, template['data'])
self.assertEqual(problem.display_name, template['metadata']['display_name'])
self.assertEqual(problem.markdown, template['metadata']['markdown'])
def test_create_item_negative(self):
"""
Negative tests for create_item
"""
# non-existent boilerplate: creates a default
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'category': 'problem',
'boilerplate': 'nosuchboilerplate.yaml'
}),
content_type="application/json"
)
self.assertEqual(resp.status_code, 200)
class TestEditItem(CourseTestCase):
"""
Test contentstore.views.item.save_item
"""
def response_id(self, response):
"""
Get the id from the response payload
:param response:
"""
parsed = json.loads(response.content)
return parsed['id']
def setUp(self):
""" Creates the test course structure and a couple problems to 'edit'. """
super(TestEditItem, self).setUp()
# create a chapter
display_name = 'chapter created'
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': self.course.location.url(),
'display_name': display_name,
'category': 'chapter'
}),
content_type="application/json"
)
chap_location = self.response_id(resp)
resp = self.client.post(
reverse('create_item'),
json.dumps(
{'parent_location': chap_location,
'category': 'sequential'
}),
content_type="application/json"
)
self.seq_location = self.response_id(resp)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.client.post(
reverse('create_item'),
json.dumps({'parent_location': self.seq_location,
'category': 'problem',
'boilerplate': template_id
}),
content_type="application/json"
)
self.problems = [self.response_id(resp)]
def test_delete_field(self):
"""
Sending null in for a field 'deletes' it
"""
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': 'onreset'}
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertEqual(problem.rerandomize, 'onreset')
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'metadata': {'rerandomize': None}
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertEqual(problem.rerandomize, 'never')
def test_null_field(self):
"""
Sending null in for a field 'deletes' it
"""
problem = modulestore('draft').get_item(self.problems[0])
self.assertIsNotNone(problem.markdown)
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.problems[0],
'nullout': ['markdown']
}),
content_type="application/json"
)
problem = modulestore('draft').get_item(self.problems[0])
self.assertIsNone(problem.markdown)
def test_date_fields(self):
"""
Test setting due & start dates on sequential
"""
sequential = modulestore().get_item(self.seq_location)
self.assertIsNone(sequential.lms.due)
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.seq_location,
'metadata': {'due': '2010-11-22T04:00Z'}
}),
content_type="application/json"
)
sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.client.post(
reverse('save_item'),
json.dumps({
'id': self.seq_location,
'metadata': {'start': '2010-09-12T14:00Z'}
}),
content_type="application/json"
)
sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.assertEqual(sequential.lms.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
...@@ -62,6 +62,9 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -62,6 +62,9 @@ class TextbookIndexTestCase(CourseTestCase):
} }
] ]
self.course.pdf_textbooks = content self.course.pdf_textbooks = content
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
self.course.save()
store = get_modulestore(self.course.location) store = get_modulestore(self.course.location)
store.update_metadata(self.course.location, own_metadata(self.course)) store.update_metadata(self.course.location, own_metadata(self.course))
...@@ -220,6 +223,9 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -220,6 +223,9 @@ class TextbookByIdTestCase(CourseTestCase):
'tid': 2, 'tid': 2,
}) })
self.course.pdf_textbooks = [self.textbook1, self.textbook2] self.course.pdf_textbooks = [self.textbook1, self.textbook2]
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
self.course.save()
self.store = get_modulestore(self.course.location) self.store = get_modulestore(self.course.location)
self.store.update_metadata(self.course.location, own_metadata(self.course)) self.store.update_metadata(self.course.location, own_metadata(self.course))
self.url_nonexist = reverse('textbook_by_id', kwargs={ self.url_nonexist = reverse('textbook_by_id', kwargs={
......
...@@ -54,7 +54,6 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -54,7 +54,6 @@ class CourseTestCase(ModuleStoreTestCase):
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
self.course = CourseFactory.create( self.course = CourseFactory.create(
template='i4x://edx/templates/course/Empty',
org='MITx', org='MITx',
number='999', number='999',
display_name='Robot Super Course', display_name='Robot Super Course',
......
...@@ -9,23 +9,24 @@ import copy ...@@ -9,23 +9,24 @@ import copy
import logging import logging
import re import re
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
from django.utils.translation import ugettext as _
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# In order to instantiate an open ended tab automatically, need to have this data # In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} OPEN_ENDED_PANEL = {"name": _("Open Ended Panel"), "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"} NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]]) EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location): def get_modulestore(category_or_location):
""" """
Returns the correct modulestore to use for modifying the specified location Returns the correct modulestore to use for modifying the specified location
""" """
if not isinstance(location, Location): if isinstance(category_or_location, Location):
location = Location(location) category_or_location = category_or_location.category
if location.category in DIRECT_ONLY_CATEGORIES: if category_or_location in DIRECT_ONLY_CATEGORIES:
return modulestore('direct') return modulestore('direct')
else: else:
return modulestore() return modulestore()
......
...@@ -13,7 +13,7 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -13,7 +13,7 @@ from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile from django.core.files.temp import NamedTemporaryFile
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST, require_http_methods
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content from cache_toolbox.core import del_cached_content
...@@ -249,6 +249,7 @@ def remove_asset(request, org, course, name): ...@@ -249,6 +249,7 @@ def remove_asset(request, org, course, name):
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT"))
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
""" """
...@@ -256,7 +257,7 @@ def import_course(request, org, course, name): ...@@ -256,7 +257,7 @@ def import_course(request, org, course, name):
""" """
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST': if request.method in ('POST', 'PUT'):
filename = request.FILES['course-data'].name filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'): if not filename.endswith('.tar.gz'):
......
...@@ -7,11 +7,11 @@ from django.views.decorators.http import require_http_methods ...@@ -7,11 +7,11 @@ from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse from ..utils import get_modulestore, get_url_reverse
from .access import get_location_and_verify_access from .access import get_location_and_verify_access
from xmodule.course_module import CourseDescriptor
__all__ = ['get_checklists', 'update_checklist'] __all__ = ['get_checklists', 'update_checklist']
...@@ -28,13 +28,11 @@ def get_checklists(request, org, course, name): ...@@ -28,13 +28,11 @@ def get_checklists(request, org, course, name):
modulestore = get_modulestore(location) modulestore = get_modulestore(location)
course_module = modulestore.get_item(location) course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template. # If course was created before checklists were introduced, copy them over from the template.
copied = False copied = False
if not course_module.checklists: if not course_module.checklists:
course_module.checklists = template_module.checklists course_module.checklists = CourseDescriptor.checklists.default
copied = True copied = True
checklists, modified = expand_checklist_action_urls(course_module) checklists, modified = expand_checklist_action_urls(course_module)
......
...@@ -26,6 +26,8 @@ from models.settings.course_grading import CourseGradingModel ...@@ -26,6 +26,8 @@ from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse from .requests import _xmodule_recurse
from .access import has_access from .access import has_access
from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError
__all__ = ['OPEN_ENDED_COMPONENT_TYPES', __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY', 'ADVANCED_COMPONENT_POLICY_KEY',
...@@ -101,7 +103,7 @@ def edit_subsection(request, location): ...@@ -101,7 +103,7 @@ def edit_subsection(request, location):
return render_to_response('edit_subsection.html', return render_to_response('edit_subsection.html',
{'subsection': item, {'subsection': item,
'context_course': course, 'context_course': course,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'new_unit_category': 'vertical',
'lms_link': lms_link, 'lms_link': lms_link,
'preview_link': preview_link, 'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
...@@ -134,10 +136,26 @@ def edit_unit(request, location): ...@@ -134,10 +136,26 @@ def edit_unit(request, location):
item = modulestore().get_item(location, depth=1) item = modulestore().get_item(location, depth=1)
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list) component_templates = defaultdict(list)
for category in COMPONENT_TYPES:
component_class = XModuleDescriptor.load_class(category)
# add the default template
component_templates[category].append((
component_class.display_name.default or 'Blank',
category,
False, # No defaults have markdown (hardcoded current default)
None # no boilerplate for overrides
))
# add boilerplates
for template in component_class.templates():
component_templates[category].append((
template['metadata'].get('display_name'),
category,
template['metadata'].get('markdown') is not None,
template.get('template_id')
))
# Check if there are any advanced modules specified in the course policy. These modules # Check if there are any advanced modules specified in the course policy. These modules
# should be specified as a list of strings, where the strings are the names of the modules # should be specified as a list of strings, where the strings are the names of the modules
...@@ -145,29 +163,29 @@ def edit_unit(request, location): ...@@ -145,29 +163,29 @@ def edit_unit(request, location):
course_advanced_keys = course.advanced_modules course_advanced_keys = course.advanced_modules
# Set component types according to course policy file # Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list): if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES] for category in course_advanced_keys:
if len(course_advanced_keys) > 0: if category in ADVANCED_COMPONENT_TYPES:
component_types.append(ADVANCED_COMPONENT_CATEGORY) # Do I need to allow for boilerplates or just defaults on the class? i.e., can an advanced
# have more than one entry in the menu? one for default and others for prefilled boilerplates?
try:
component_class = XModuleDescriptor.load_class(category)
component_templates['advanced'].append((
component_class.display_name.default or category,
category,
False,
None # don't override default data
))
except PluginMissingError:
# dhm: I got this once but it can happen any time the course author configures
# an advanced component which does not exist on the server. This code here merely
# prevents any authors from trying to instantiate the non-existent component type
# by not showing it in the menu
pass
else: else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys)) log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates:
category = template.location.category
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
# This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None
))
components = [ components = [
component.location.url() component.location.url()
for component for component
...@@ -219,7 +237,7 @@ def edit_unit(request, location): ...@@ -219,7 +237,7 @@ def edit_unit(request, location):
'subsection': containing_subsection, 'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None, 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section, 'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'new_unit_category': 'vertical',
'unit_state': unit_state, 'unit_state': unit_state,
'published_date': get_default_time_display(item.cms.published_date) if item.cms.published_date is not None else None 'published_date': get_default_time_display(item.cms.published_date) if item.cms.published_date is not None else None
}) })
...@@ -227,6 +245,7 @@ def edit_unit(request, location): ...@@ -227,6 +245,7 @@ def edit_unit(request, location):
@expect_json @expect_json
@login_required @login_required
@require_http_methods(("GET", "POST", "PUT"))
@ensure_csrf_cookie @ensure_csrf_cookie
def assignment_type_update(request, org, course, category, name): def assignment_type_update(request, org, course, category, name):
''' '''
...@@ -238,7 +257,7 @@ def assignment_type_update(request, org, course, category, name): ...@@ -238,7 +257,7 @@ def assignment_type_update(request, org, course, category, name):
if request.method == 'GET': if request.method == 'GET':
return JsonResponse(CourseGradingModel.get_section_grader_type(location)) return JsonResponse(CourseGradingModel.get_section_grader_type(location))
elif request.method == 'POST': # post or put, doesn't matter. elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST)) return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST))
...@@ -253,7 +272,7 @@ def create_draft(request): ...@@ -253,7 +272,7 @@ def create_draft(request):
# This clones the existing item location to a draft location (the draft is implicit, # This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore) # because modulestore is a Draft modulestore)
modulestore().clone_item(location, location) modulestore().convert_to_draft(location)
return HttpResponse() return HttpResponse()
......
""" """
Views related to operations on course objects Views related to operations on course objects
""" """
#pylint: disable=W0402
import json import json
import random import random
import string import string # pylint: disable=W0402
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
...@@ -43,8 +42,8 @@ from .component import ( ...@@ -43,8 +42,8 @@ from .component import (
ADVANCED_COMPONENT_POLICY_KEY) ADVANCED_COMPONENT_POLICY_KEY)
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC from xmodule.html_module import AboutDescriptor
__all__ = ['course_index', 'create_new_course', 'course_info', __all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings', 'course_info_updates', 'get_course_settings',
'course_config_graders_page', 'course_config_graders_page',
...@@ -82,10 +81,11 @@ def course_index(request, org, course, name): ...@@ -82,10 +81,11 @@ def course_index(request, org, course, name):
'sections': sections, 'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location, 'parent_location': course.location,
'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'), 'new_section_category': 'chapter',
'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point... 'new_subsection_category': 'sequential',
'upload_asset_callback_url': upload_asset_callback_url, 'upload_asset_callback_url': upload_asset_callback_url,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') 'new_unit_category': 'vertical',
'category': 'vertical'
}) })
...@@ -98,12 +98,6 @@ def create_new_course(request): ...@@ -98,12 +98,6 @@ def create_new_course(request):
if not is_user_in_creator_group(request.user): if not is_user_in_creator_group(request.user):
raise PermissionDenied() raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and
# the other with this method, then compare them to make sure they are
# equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org') org = request.POST.get('org')
number = request.POST.get('number') number = request.POST.get('number')
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
...@@ -121,29 +115,31 @@ def create_new_course(request): ...@@ -121,29 +115,31 @@ def create_new_course(request):
existing_course = modulestore('direct').get_item(dest_location) existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError: except ItemNotFoundError:
pass pass
if existing_course is not None: if existing_course is not None:
return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'}) return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'})
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location) courses = modulestore().get_items(course_search_location)
if len(courses) > 0: if len(courses) > 0:
return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'}) return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'})
new_course = modulestore('direct').clone_item(template, dest_location) # instantiate the CourseDescriptor and then persist it
# note: no system to pass
# clone a default 'about' module as well if display_name is None:
metadata = {}
about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) else:
dest_about_location = dest_location._replace(category='about', name='overview') metadata = {'display_name': display_name}
modulestore('direct').clone_item(about_template_location, dest_about_location) modulestore('direct').create_and_save_xmodule(dest_location, metadata=metadata)
new_course = modulestore('direct').get_item(dest_location)
if display_name is not None:
new_course.display_name = display_name # clone a default 'about' overview module as well
dest_about_location = dest_location.replace(category='about', name='overview')
# set a default start date to now overview_template = AboutDescriptor.get_template('overview.yaml')
new_course.start = datetime.datetime.now(UTC()) modulestore('direct').create_and_save_xmodule(
dest_about_location,
system=new_course.system,
definition_data=overview_template.get('data')
)
initialize_course_tabs(new_course) initialize_course_tabs(new_course)
...@@ -179,6 +175,7 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -179,6 +175,7 @@ def course_info(request, org, course, name, provided_id=None):
@expect_json @expect_json
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_info_updates(request, org, course, provided_id=None): def course_info_updates(request, org, course, provided_id=None):
...@@ -209,7 +206,7 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -209,7 +206,7 @@ def course_info_updates(request, org, course, provided_id=None):
except: except:
return HttpResponseBadRequest("Failed to delete", return HttpResponseBadRequest("Failed to delete",
content_type="text/plain") content_type="text/plain")
elif request.method == 'POST': elif request.method in ('POST', 'PUT'): # can be either and sometimes django is rewriting one to the other
try: try:
return JsonResponse(update_course_updates(location, request.POST, provided_id)) return JsonResponse(update_course_updates(location, request.POST, provided_id))
except: except:
...@@ -303,7 +300,7 @@ def course_settings_updates(request, org, course, name, section): ...@@ -303,7 +300,7 @@ def course_settings_updates(request, org, course, name, section):
if request.method == 'GET': if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return JsonResponse(manager.fetch(Location(['i4x', org, course, 'course', name])), encoder=CourseSettingsEncoder) return JsonResponse(manager.fetch(Location(['i4x', org, course, 'course', name])), encoder=CourseSettingsEncoder)
elif request.method == 'POST': # post or put, doesn't matter. elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
return JsonResponse(manager.update_from_json(request.POST), encoder=CourseSettingsEncoder) return JsonResponse(manager.update_from_json(request.POST), encoder=CourseSettingsEncoder)
...@@ -482,7 +479,7 @@ def textbook_index(request, org, course, name): ...@@ -482,7 +479,7 @@ def textbook_index(request, org, course, name):
if request.is_ajax(): if request.is_ajax():
if request.method == 'GET': if request.method == 'GET':
return JsonResponse(course_module.pdf_textbooks) return JsonResponse(course_module.pdf_textbooks)
elif request.method == 'POST': elif request.method in ('POST', 'PUT'): # can be either and sometimes django is rewriting one to the other
try: try:
textbooks = validate_textbooks_json(request.body) textbooks = validate_textbooks_json(request.body)
except TextbookValidationError as err: except TextbookValidationError as err:
...@@ -498,6 +495,9 @@ def textbook_index(request, org, course, name): ...@@ -498,6 +495,9 @@ def textbook_index(request, org, course, name):
if not any(tab['type'] == 'pdf_textbooks' for tab in course_module.tabs): if not any(tab['type'] == 'pdf_textbooks' for tab in course_module.tabs):
course_module.tabs.append({"type": "pdf_textbooks"}) course_module.tabs.append({"type": "pdf_textbooks"})
course_module.pdf_textbooks = textbooks course_module.pdf_textbooks = textbooks
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course_module.save()
store.update_metadata(course_module.location, own_metadata(course_module)) store.update_metadata(course_module.location, own_metadata(course_module))
return JsonResponse(course_module.pdf_textbooks) return JsonResponse(course_module.pdf_textbooks)
else: else:
...@@ -544,6 +544,9 @@ def create_textbook(request, org, course, name): ...@@ -544,6 +544,9 @@ def create_textbook(request, org, course, name):
tabs = course_module.tabs tabs = course_module.tabs
tabs.append({"type": "pdf_textbooks"}) tabs.append({"type": "pdf_textbooks"})
course_module.tabs = tabs course_module.tabs = tabs
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course_module.save()
store.update_metadata(course_module.location, own_metadata(course_module)) store.update_metadata(course_module.location, own_metadata(course_module))
resp = JsonResponse(textbook, status=201) resp = JsonResponse(textbook, status=201)
resp["Location"] = reverse("textbook_by_id", kwargs={ resp["Location"] = reverse("textbook_by_id", kwargs={
...@@ -577,7 +580,7 @@ def textbook_by_id(request, org, course, name, tid): ...@@ -577,7 +580,7 @@ def textbook_by_id(request, org, course, name, tid):
if not textbook: if not textbook:
return JsonResponse(status=404) return JsonResponse(status=404)
return JsonResponse(textbook) return JsonResponse(textbook)
elif request.method in ('POST', 'PUT'): elif request.method in ('POST', 'PUT'): # can be either and sometimes django is rewriting one to the other
try: try:
new_textbook = validate_textbook_json(request.body) new_textbook = validate_textbook_json(request.body)
except TextbookValidationError as err: except TextbookValidationError as err:
...@@ -587,10 +590,13 @@ def textbook_by_id(request, org, course, name, tid): ...@@ -587,10 +590,13 @@ def textbook_by_id(request, org, course, name, tid):
i = course_module.pdf_textbooks.index(textbook) i = course_module.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks = course_module.pdf_textbooks[0:i]
new_textbooks.append(new_textbook) new_textbooks.append(new_textbook)
new_textbooks.extend(course_module.pdf_textbooks[i+1:]) new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = new_textbooks course_module.pdf_textbooks = new_textbooks
else: else:
course_module.pdf_textbooks.append(new_textbook) course_module.pdf_textbooks.append(new_textbook)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course_module.save()
store.update_metadata(course_module.location, own_metadata(course_module)) store.update_metadata(course_module.location, own_metadata(course_module))
return JsonResponse(new_textbook, status=201) return JsonResponse(new_textbook, status=201)
elif request.method == 'DELETE': elif request.method == 'DELETE':
...@@ -598,7 +604,8 @@ def textbook_by_id(request, org, course, name, tid): ...@@ -598,7 +604,8 @@ def textbook_by_id(request, org, course, name, tid):
return JsonResponse(status=404) return JsonResponse(status=404)
i = course_module.pdf_textbooks.index(textbook) i = course_module.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks = course_module.pdf_textbooks[0:i]
new_textbooks.extend(course_module.pdf_textbooks[i+1:]) new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = new_textbooks course_module.pdf_textbooks = new_textbooks
course_module.save()
store.update_metadata(course_module.location, own_metadata(course_module)) store.update_metadata(course_module.location, own_metadata(course_module))
return JsonResponse() return JsonResponse()
...@@ -13,16 +13,26 @@ from util.json_request import expect_json ...@@ -13,16 +13,26 @@ from util.json_request import expect_json
from ..utils import get_modulestore from ..utils import get_modulestore
from .access import has_access from .access import has_access
from .requests import _xmodule_recurse from .requests import _xmodule_recurse
from xmodule.x_module import XModuleDescriptor
__all__ = ['save_item', 'clone_item', 'delete_item'] __all__ = ['save_item', 'create_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy # cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required @login_required
@expect_json @expect_json
def save_item(request): def save_item(request):
"""
Will carry a json payload with these possible fields
:id (required): the id
:data (optional): the new value for the data
:metadata (optional): new values for the metadata fields.
Any whose values are None will be deleted not set to None! Absent ones will be left alone
:nullout (optional): which metadata fields to set to None
"""
# The nullout is a bit of a temporary copout until we can make module_edit.coffee and the metadata editors a
# little smarter and able to pass something more akin to {unset: [field, field]}
item_location = request.POST['id'] item_location = request.POST['id']
# check permissions for this user within this course # check permissions for this user within this course
...@@ -42,59 +52,98 @@ def save_item(request): ...@@ -42,59 +52,98 @@ def save_item(request):
children = request.POST['children'] children = request.POST['children']
store.update_children(item_location, children) store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the # cdodge: also commit any metadata which might have been passed along
# POST from the client, if it is there if request.POST.get('nullout') is not None or request.POST.get('metadata') is not None:
# NOTE, that the postback is not the complete metadata, as there's system metadata which is # the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and # not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata # 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location) existing_item = modulestore().get_item(item_location)
for metadata_key in request.POST.get('nullout', []):
# [dhm] see comment on _get_xblock_field
_get_xblock_field(existing_item, metadata_key).write_to(existing_item, None)
# update existing metadata with submitted metadata (which can be partial) # update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
for metadata_key, value in posted_metadata.items(): # the intent is to make it None, use the nullout field
for metadata_key, value in request.POST.get('metadata', {}).items():
if posted_metadata[metadata_key] is None: # [dhm] see comment on _get_xblock_field
# remove both from passed in collection as well as the collection read in from the modulestore field = _get_xblock_field(existing_item, metadata_key)
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key] if value is None:
del posted_metadata[metadata_key] field.delete_from(existing_item)
else: else:
existing_item._model_data[metadata_key] = value value = field.from_json(value)
field.write_to(existing_item, value)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
existing_item.save()
# commit to datastore # commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item)) store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse() return HttpResponse()
# [DHM] A hack until we implement a permanent soln. Proposed perm solution is to make namespace fields also top level
# fields in xblocks rather than requiring dereference through namespace but we'll need to consider whether there are
# plausible use cases for distinct fields w/ same name in different namespaces on the same blocks.
# The idea is that consumers of the xblock, and particularly the web client, shouldn't know about our internal
# representation (namespaces as means of decorating all modules).
# Given top-level access, the calls can simply be setattr(existing_item, field, value) ...
# Really, this method should be elsewhere (e.g., xblock). We also need methods for has_value (v is_default)...
def _get_xblock_field(xblock, field_name):
"""
A temporary function to get the xblock field either from the xblock or one of its namespaces by name.
:param xblock:
:param field_name:
"""
def find_field(fields):
for field in fields:
if field.name == field_name:
return field
found = find_field(xblock.fields)
if found:
return found
for namespace in xblock.namespaces:
found = find_field(getattr(xblock, namespace).fields)
if found:
return found
@login_required @login_required
@expect_json @expect_json
def clone_item(request): def create_item(request):
parent_location = Location(request.POST['parent_location']) parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template']) category = request.POST['category']
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location): if not has_access(request.user, parent_location):
raise PermissionDenied() raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location) parent = get_modulestore(category).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex) dest_location = parent_location.replace(category=category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location) # get the metadata, display_name, and definition from the request
metadata = {}
data = None
template_id = request.POST.get('boilerplate')
if template_id is not None:
clz = XModuleDescriptor.load_class(category)
if clz is not None:
template = clz.get_template(template_id)
if template is not None:
metadata = template.get('metadata', {})
data = template.get('data')
# replace the display name with an optional parameter passed in from the caller
if display_name is not None: if display_name is not None:
new_item.display_name = display_name metadata['display_name'] = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item)) get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data,
metadata=metadata, system=parent.system)
if new_item.location.category not in DETACHED_CATEGORIES: if category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()]) get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()})) return HttpResponse(json.dumps({'id': dest_location.url()}))
......
...@@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse ...@@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule_modifiers import replace_static_urls, wrap_xmodule, save_module # pylint: disable=F0401
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
...@@ -47,6 +47,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -47,6 +47,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
# Save any module data that has changed to the underlying KeyValueStore
instance.save()
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
...@@ -166,6 +168,11 @@ def load_preview_module(request, preview_id, descriptor): ...@@ -166,6 +168,11 @@ def load_preview_module(request, preview_id, descriptor):
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None]) course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
) )
module.get_html = save_module(
module.get_html,
module
)
return module return module
......
...@@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore ...@@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item, get_modulestore from ..utils import get_course_for_item, get_modulestore
from .access import get_location_and_verify_access from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static'] __all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages']
def initialize_course_tabs(course): def initialize_course_tabs(course):
...@@ -76,6 +76,9 @@ def reorder_static_tabs(request): ...@@ -76,6 +76,9 @@ def reorder_static_tabs(request):
# OK, re-assemble the static tabs in the new order # OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs course.tabs = reordered_tabs
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course.save()
modulestore('direct').update_metadata(course.location, own_metadata(course)) modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse() return HttpResponse()
...@@ -127,7 +130,3 @@ def static_pages(request, org, course, coursename): ...@@ -127,7 +130,3 @@ def static_pages(request, org, course, coursename):
return render_to_response('static-pages.html', { return render_to_response('static-pages.html', {
'context_course': course, 'context_course': course,
}) })
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
...@@ -2,12 +2,12 @@ from django.conf import settings ...@@ -2,12 +2,12 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from django.core.context_processors import csrf from django.core.context_processors import csrf
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
...@@ -29,6 +29,7 @@ def index(request): ...@@ -29,6 +29,7 @@ def index(request):
# filter out courses that we don't have access too # filter out courses that we don't have access too
def course_filter(course): def course_filter(course):
return (has_access(request.user, course.location) return (has_access(request.user, course.location)
# TODO remove this condition when templates purged from db
and course.location.course != 'templates' and course.location.course != 'templates'
and course.location.org != '' and course.location.org != ''
and course.location.course != '' and course.location.course != ''
...@@ -36,7 +37,6 @@ def index(request): ...@@ -36,7 +37,6 @@ def index(request):
courses = filter(course_filter, courses) courses = filter(course_filter, courses)
return render_to_response('index.html', { return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name, 'courses': [(course.display_name,
get_url_reverse('CourseOutline', course), get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id)) get_lms_link_for_item(course.location, course_id=course.location.course_id))
...@@ -49,7 +49,6 @@ def index(request): ...@@ -49,7 +49,6 @@ def index(request):
@require_POST @require_POST
@ensure_csrf_cookie
@login_required @login_required
def request_course_creator(request): def request_course_creator(request):
""" """
...@@ -94,7 +93,7 @@ def add_user(request, location): ...@@ -94,7 +93,7 @@ def add_user(request, location):
if not email: if not email:
msg = { msg = {
'Status': 'Failed', 'Status': 'Failed',
'ErrMsg': 'Please specify an email address.', 'ErrMsg': _('Please specify an email address.'),
} }
return JsonResponse(msg, 400) return JsonResponse(msg, 400)
...@@ -108,7 +107,7 @@ def add_user(request, location): ...@@ -108,7 +107,7 @@ def add_user(request, location):
if user is None: if user is None:
msg = { msg = {
'Status': 'Failed', 'Status': 'Failed',
'ErrMsg': "Could not find user by email address '{0}'.".format(email), 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email),
} }
return JsonResponse(msg, 404) return JsonResponse(msg, 404)
...@@ -116,7 +115,7 @@ def add_user(request, location): ...@@ -116,7 +115,7 @@ def add_user(request, location):
if not user.is_active: if not user.is_active:
msg = { msg = {
'Status': 'Failed', 'Status': 'Failed',
'ErrMsg': 'User {0} has registered but has not yet activated his/her account.'.format(email), 'ErrMsg': _('User {email} has registered but has not yet activated his/her account.').format(email=email),
} }
return JsonResponse(msg, 400) return JsonResponse(msg, 400)
...@@ -145,7 +144,7 @@ def remove_user(request, location): ...@@ -145,7 +144,7 @@ def remove_user(request, location):
if user is None: if user is None:
msg = { msg = {
'Status': 'Failed', 'Status': 'Failed',
'ErrMsg': "Could not find user by email address '{0}'.".format(email), 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email),
} }
return JsonResponse(msg, 404) return JsonResponse(msg, 404)
......
...@@ -122,6 +122,10 @@ class CourseDetails(object): ...@@ -122,6 +122,10 @@ class CourseDetails(object):
descriptor.enrollment_end = converted descriptor.enrollment_end = converted
if dirty: if dirty:
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor)) get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
......
...@@ -7,9 +7,12 @@ class CourseGradingModel(object): ...@@ -7,9 +7,12 @@ class CourseGradingModel(object):
""" """
Basically a DAO and Model combo for CRUD operations pertaining to grading policy. Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
""" """
# Within this class, allow access to protected members of client classes.
# This comes up when accessing kvs data and caches during kvs saves and modulestore writes.
# pylint: disable=W0212
def __init__(self, course_descriptor): def __init__(self, course_descriptor):
self.course_location = course_descriptor.location self.course_location = course_descriptor.location
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
...@@ -81,15 +84,18 @@ class CourseGradingModel(object): ...@@ -81,15 +84,18 @@ class CourseGradingModel(object):
Decode the json into CourseGradingModel and save any changes. Returns the modified model. Decode the json into CourseGradingModel and save any changes. Returns the modified model.
Probably not the usual path for updates as it's too coarse grained. Probably not the usual path for updates as it's too coarse grained.
""" """
course_location = jsondict['course_location'] course_location = Location(jsondict['course_location'])
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']] graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
descriptor.raw_grader = graders_parsed descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs'] descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor.xblock_kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period']) CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location) return CourseGradingModel.fetch(course_location)
...@@ -116,6 +122,9 @@ class CourseGradingModel(object): ...@@ -116,6 +122,9 @@ class CourseGradingModel(object):
else: else:
descriptor.raw_grader.append(grader) descriptor.raw_grader.append(grader)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -131,6 +140,10 @@ class CourseGradingModel(object): ...@@ -131,6 +140,10 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs descriptor.grade_cutoffs = cutoffs
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return cutoffs return cutoffs
...@@ -156,6 +169,10 @@ class CourseGradingModel(object): ...@@ -156,6 +169,10 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.lms.graceperiod = grace_timedelta descriptor.lms.graceperiod = grace_timedelta
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
...@@ -172,22 +189,11 @@ class CourseGradingModel(object): ...@@ -172,22 +189,11 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index] del descriptor.raw_grader[index]
# force propagation to definition # force propagation to definition
descriptor.raw_grader = descriptor.raw_grader descriptor.raw_grader = descriptor.raw_grader
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
def delete_cutoffs(course_location, cutoffs):
"""
Resets the cutoffs to the defaults
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) # Save the data that we've just changed to the underlying
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS'] # MongoKeyValueStore before we update the mongo datastore.
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
@staticmethod @staticmethod
def delete_grace_period(course_location): def delete_grace_period(course_location):
...@@ -199,6 +205,10 @@ class CourseGradingModel(object): ...@@ -199,6 +205,10 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
del descriptor.lms.graceperiod del descriptor.lms.graceperiod
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
...@@ -209,7 +219,7 @@ class CourseGradingModel(object): ...@@ -209,7 +219,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded', return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location, "location": location,
"id": 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
@staticmethod @staticmethod
...@@ -225,6 +235,9 @@ class CourseGradingModel(object): ...@@ -225,6 +235,9 @@ class CourseGradingModel(object):
del descriptor.lms.format del descriptor.lms.format
del descriptor.lms.graded del descriptor.lms.graded
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata) get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod @staticmethod
...@@ -232,7 +245,7 @@ class CourseGradingModel(object): ...@@ -232,7 +245,7 @@ class CourseGradingModel(object):
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod rawgrace = descriptor.lms.graceperiod
if rawgrace: if rawgrace:
hours_from_days = rawgrace.days*24 hours_from_days = rawgrace.days * 24
seconds = rawgrace.seconds seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600) hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds hours = hours_from_days + hours_from_seconds
......
...@@ -76,6 +76,9 @@ class CourseMetadata(object): ...@@ -76,6 +76,9 @@ class CourseMetadata(object):
setattr(descriptor.lms, key, value) setattr(descriptor.lms, key, value)
if dirty: if dirty:
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor)) own_metadata(descriptor))
...@@ -97,6 +100,10 @@ class CourseMetadata(object): ...@@ -97,6 +100,10 @@ class CourseMetadata(object):
elif hasattr(descriptor.lms, key): elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key) delattr(descriptor.lms, key)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor)) own_metadata(descriptor))
......
...@@ -92,6 +92,7 @@ LOG_DIR = ENV_TOKENS['LOG_DIR'] ...@@ -92,6 +92,7 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
CACHES = ENV_TOKENS['CACHES'] CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
SESSION_ENGINE = ENV_TOKENS.get('SESSION_ENGINE', SESSION_ENGINE)
# allow for environments to specify what cookie name our login subsystem should use # allow for environments to specify what cookie name our login subsystem should use
# this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can # this is to fix a bug regarding simultaneous logins between edx.org and edge.edx.org which can
...@@ -122,6 +123,10 @@ LOGGING = get_logger_config(LOG_DIR, ...@@ -122,6 +123,10 @@ LOGGING = get_logger_config(LOG_DIR,
debug=False, debug=False,
service_variant=SERVICE_VARIANT) service_variant=SERVICE_VARIANT)
#theming start:
PLATFORM_NAME = ENV_TOKENS.get('PLATFORM_NAME', 'edX')
################ SECURE AUTH ITEMS ############################### ################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file: with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
......
...@@ -33,6 +33,10 @@ MODULESTORE = { ...@@ -33,6 +33,10 @@ MODULESTORE = {
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
},
'split': {
'ENGINE': 'xmodule.modulestore.split_mongo.SplitMongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
......
...@@ -63,6 +63,10 @@ MODULESTORE = { ...@@ -63,6 +63,10 @@ MODULESTORE = {
'draft': { 'draft': {
'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore', 'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
'OPTIONS': MODULESTORE_OPTIONS 'OPTIONS': MODULESTORE_OPTIONS
},
'split': {
'ENGINE': 'xmodule.modulestore.split_mongo.SplitMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
} }
} }
......
...@@ -6,15 +6,15 @@ ...@@ -6,15 +6,15 @@
class="course-checklist" class="course-checklist"
<% } %> <% } %>
id="<%= 'course-checklist' + checklistIndex %>"> id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %> <span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="width: <%= percentChecked %>%;">
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>"> <%= _.template(gettext("{number}% of checklists completed"), {number: '<span class="int">' + percentChecked + '</span>'}, {interpolate: /\{(.+?)\}/g}) %>
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span> </span></span>
<header> <header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist"> <h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="icon-caret-down ui-toggle-expansion"></i> <i class="icon-caret-down ui-toggle-expansion"></i>
<%= checklistShortDescription %></h3> <%= checklistShortDescription %></h3>
<span class="checklist-status status"> <span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span> <%= gettext("Tasks Completed:") %> <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="icon-ok"></i> <i class="icon-ok"></i>
</span> </span>
</header> </header>
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
<li class="action-item"> <li class="action-item">
<a href="<%= item['action_url'] %>" class="action action-primary" <a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %> <% if (item['action_external']) { %>
rel="external" title="This link will open in a new browser window/tab" rel="external" title="<%= gettext("This link will open in a new browser window/tab") %>"
<% } %> <% } %>
><%= item['action_text'] %></a> ><%= item['action_text'] %></a>
</li> </li>
......
<a href="#" class="edit-button"><span class="edit-icon"></span>Edit</a> <a href="#" class="edit-button"><span class="edit-icon"></span>Edit</a>
<h2>Course Handouts</h2> <h2 class="title">Course Handouts</h2>
<%if (model.get('data') != null) { %> <%if (model.get('data') != null) { %>
<div class="handouts-content"> <div class="handouts-content">
<%= model.get('data') %> <%= model.get('data') %>
</div> </div>
<% } else {%> <% } else {%>
<p>You have no handouts defined</p> <p>${_("You have no handouts defined")}</p>
<% } %> <% } %>
<form class="edit-handouts-form" style="display: block;"> <form class="edit-handouts-form" style="display: block;">
<div class="row"> <div class="row">
......
{ {
"static_files": [ "static_files": [
"../jsi18n/",
"js/vendor/RequireJS.js", "js/vendor/RequireJS.js",
"js/vendor/jquery.min.js", "js/vendor/jquery.min.js",
"js/vendor/jquery-ui.min.js", "js/vendor/jquery-ui.min.js",
......
...@@ -56,14 +56,15 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -56,14 +56,15 @@ class CMS.Views.ModuleEdit extends Backbone.View
changedMetadata: -> changedMetadata: ->
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata()) return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
cloneTemplate: (parent, template) -> createItem: (parent, payload) ->
$.post("/clone_item", { payload.parent_location = parent
parent_location: parent $.post(
template: template "/create_item"
}, (data) => payload
@model.set(id: data.id) (data) =>
@$el.data('id', data.id) @model.set(id: data.id)
@render() @$el.data('id', data.id)
@render()
) )
render: -> render: ->
......
...@@ -55,9 +55,9 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -55,9 +55,9 @@ class CMS.Views.TabsEdit extends Backbone.View
editor.$el.removeClass('new') editor.$el.removeClass('new')
, 500) , 500)
editor.cloneTemplate( editor.createItem(
@model.get('id'), @model.get('id'),
'i4x://edx/templates/static_tab/Empty' {category: 'static_tab'}
) )
analytics.track "Added Static Page", analytics.track "Added Static Page",
......
...@@ -89,9 +89,9 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -89,9 +89,9 @@ class CMS.Views.UnitEdit extends Backbone.View
@$newComponentItem.before(editor.$el) @$newComponentItem.before(editor.$el)
editor.cloneTemplate( editor.createItem(
@$el.data('id'), @$el.data('id'),
$(event.currentTarget).data('location') $(event.currentTarget).data()
) )
analytics.track "Added a Component", analytics.track "Added a Component",
......
...@@ -79,10 +79,10 @@ $(document).ready(function() { ...@@ -79,10 +79,10 @@ $(document).ready(function() {
}); });
// general link management - new window/tab // general link management - new window/tab
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow); $('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow);
// general link management - lean modal window // general link management - lean modal window
$('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({ $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({
overlay: 0.50, overlay: 0.50,
closeButton: '.action-modal-close' closeButton: '.action-modal-close'
}); });
...@@ -199,8 +199,10 @@ function toggleSections(e) { ...@@ -199,8 +199,10 @@ function toggleSections(e) {
$section = $('.courseware-section'); $section = $('.courseware-section');
sectionCount = $section.length; sectionCount = $section.length;
$button = $(this); $button = $(this);
$labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span>'); $labelCollapsed = $('<i class="icon-arrow-up"></i> <span class="label">' +
$labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span>'); gettext('Collapse All Sections') + '</span>');
$labelExpanded = $('<i class="icon-arrow-down"></i> <span class="label">' +
gettext('Expand All Sections') + '</span>');
var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded; var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded;
$button.toggleClass('is-activated').html(buttonLabel); $button.toggleClass('is-activated').html(buttonLabel);
...@@ -251,17 +253,13 @@ function syncReleaseDate(e) { ...@@ -251,17 +253,13 @@ function syncReleaseDate(e) {
} }
function getEdxTimeFromDateTimeVals(date_val, time_val) { function getEdxTimeFromDateTimeVals(date_val, time_val) {
var edxTimeStr = null;
if (date_val != '') { if (date_val != '') {
if (time_val == '') time_val = '00:00'; if (time_val == '') time_val = '00:00';
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing return new Date(date_val + " " + time_val + "Z");
var date = Date.parse(date_val + " " + time_val);
edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
} }
return edxTimeStr; else return null;
} }
function getEdxTimeFromDateTimeInputs(date_id, time_id) { function getEdxTimeFromDateTimeInputs(date_id, time_id) {
...@@ -326,7 +324,7 @@ function saveSubsection() { ...@@ -326,7 +324,7 @@ function saveSubsection() {
$changedInput = null; $changedInput = null;
}, },
error: function() { error: function() {
showToastMessage('There has been an error while saving your changes.'); showToastMessage(gettext('There has been an error while saving your changes.'));
} }
}); });
} }
...@@ -336,7 +334,7 @@ function createNewUnit(e) { ...@@ -336,7 +334,7 @@ function createNewUnit(e) {
e.preventDefault(); e.preventDefault();
var parent = $(this).data('parent'); var parent = $(this).data('parent');
var template = $(this).data('template'); var category = $(this).data('category');
analytics.track('Created a Unit', { analytics.track('Created a Unit', {
'course': course_location_analytics, 'course': course_location_analytics,
...@@ -344,9 +342,9 @@ function createNewUnit(e) { ...@@ -344,9 +342,9 @@ function createNewUnit(e) {
}); });
$.post('/clone_item', { $.post('/create_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'category': category,
'display_name': 'New Unit' 'display_name': 'New Unit'
}, },
...@@ -372,7 +370,7 @@ function deleteSection(e) { ...@@ -372,7 +370,7 @@ function deleteSection(e) {
} }
function _deleteItem($el) { function _deleteItem($el) {
if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!')) return; if (!confirm(gettext('Are you sure you wish to delete this item. It cannot be reversed!'))) return;
var id = $el.data('id'); var id = $el.data('id');
...@@ -549,7 +547,7 @@ function saveNewSection(e) { ...@@ -549,7 +547,7 @@ function saveNewSection(e) {
var $saveButton = $(this).find('.new-section-name-save'); var $saveButton = $(this).find('.new-section-name-save');
var parent = $saveButton.data('parent'); var parent = $saveButton.data('parent');
var template = $saveButton.data('template'); var category = $saveButton.data('category');
var display_name = $(this).find('.new-section-name').val(); var display_name = $(this).find('.new-section-name').val();
analytics.track('Created a Section', { analytics.track('Created a Section', {
...@@ -557,9 +555,9 @@ function saveNewSection(e) { ...@@ -557,9 +555,9 @@ function saveNewSection(e) {
'display_name': display_name 'display_name': display_name
}); });
$.post('/clone_item', { $.post('/create_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'category': category,
'display_name': display_name, 'display_name': display_name,
}, },
...@@ -593,13 +591,12 @@ function saveNewCourse(e) { ...@@ -593,13 +591,12 @@ function saveNewCourse(e) {
e.preventDefault(); e.preventDefault();
var $newCourse = $(this).closest('.new-course'); var $newCourse = $(this).closest('.new-course');
var template = $(this).find('.new-course-save').data('template');
var org = $newCourse.find('.new-course-org').val(); var org = $newCourse.find('.new-course-org').val();
var number = $newCourse.find('.new-course-number').val(); var number = $newCourse.find('.new-course-number').val();
var display_name = $newCourse.find('.new-course-name').val(); var display_name = $newCourse.find('.new-course-name').val();
if (org == '' || number == '' || display_name == '') { if (org == '' || number == '' || display_name == '') {
alert('You must specify all fields in order to create a new course.'); alert(gettext('You must specify all fields in order to create a new course.'));
return; return;
} }
...@@ -610,7 +607,6 @@ function saveNewCourse(e) { ...@@ -610,7 +607,6 @@ function saveNewCourse(e) {
}); });
$.post('/create_new_course', { $.post('/create_new_course', {
'template': template,
'org': org, 'org': org,
'number': number, 'number': number,
'display_name': display_name 'display_name': display_name
...@@ -644,7 +640,7 @@ function addNewSubsection(e) { ...@@ -644,7 +640,7 @@ function addNewSubsection(e) {
var parent = $(this).parents("section.branch").data("id"); var parent = $(this).parents("section.branch").data("id");
$saveButton.data('parent', parent); $saveButton.data('parent', parent);
$saveButton.data('template', $(this).data('template')); $saveButton.data('category', $(this).data('category'));
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection); $newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
$cancelButton.bind('click', cancelNewSubsection); $cancelButton.bind('click', cancelNewSubsection);
...@@ -657,7 +653,7 @@ function saveNewSubsection(e) { ...@@ -657,7 +653,7 @@ function saveNewSubsection(e) {
e.preventDefault(); e.preventDefault();
var parent = $(this).find('.new-subsection-name-save').data('parent'); var parent = $(this).find('.new-subsection-name-save').data('parent');
var template = $(this).find('.new-subsection-name-save').data('template'); var category = $(this).find('.new-subsection-name-save').data('category');
var display_name = $(this).find('.new-subsection-name-input').val(); var display_name = $(this).find('.new-subsection-name-input').val();
analytics.track('Created a Subsection', { analytics.track('Created a Subsection', {
...@@ -666,9 +662,9 @@ function saveNewSubsection(e) { ...@@ -666,9 +662,9 @@ function saveNewSubsection(e) {
}); });
$.post('/clone_item', { $.post('/create_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'category': category,
'display_name': display_name 'display_name': display_name
}, },
...@@ -730,18 +726,16 @@ function saveSetSectionScheduleDate(e) { ...@@ -730,18 +726,16 @@ function saveSetSectionScheduleDate(e) {
}) })
}).success(function() { }).success(function() {
var $thisSection = $('.courseware-section[data-id="' + id + '"]'); var $thisSection = $('.courseware-section[data-id="' + id + '"]');
var format = gettext('<strong>Will Release:</strong> %(date)s at %(time)s UTC'); var html = _.template(
var willReleaseAt = interpolate(format, { '<span class="published-status">' +
'date': input_date, '<strong>' + gettext("Will Release: ") + '</strong>' +
'time': input_time gettext("<%= date %> at <%= time %> UTC") +
}, '</span>' +
true); '<a href="#" class="edit-button" data-date="<%= date %>" data-time="<%= time %>" data-id="<%= id %>">' +
$thisSection.find('.section-published-date').html( gettext("Edit") +
'<span class="published-status">' + willReleaseAt + '</span>' + '</a>',
'<a href="#" class="edit-button" ' + {date: input_date, time: input_time, id: id});
'" data-date="' + input_date + $thisSection.find('.section-published-date').html(html);
'" data-time="' + input_time +
'" data-id="' + id + '">' + gettext('Edit') + '</a>');
hideModal(); hideModal();
saving.hide(); saving.hide();
}); });
......
...@@ -38,23 +38,23 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -38,23 +38,23 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {}; var errors = {};
if (newattrs.start_date === null) { if (newattrs.start_date === null) {
errors.start_date = "The course must have an assigned start date."; errors.start_date = gettext("The course must have an assigned start date.");
} }
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date."; errors.end_date = gettext("The course end date cannot be before the course start date.");
} }
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) { if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
errors.enrollment_start = "The course start date cannot be before the enrollment start date."; errors.enrollment_start = gettext("The course start date cannot be before the enrollment start date.");
} }
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) { if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date."; errors.enrollment_end = gettext("The enrollment start date cannot be after the enrollment end date.");
} }
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) { if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment end date cannot be after the course end date."; errors.enrollment_end = gettext("The enrollment end date cannot be after the course end date.");
} }
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) { if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) { if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
errors.intro_video = "Key should only contain letters, numbers, _, or -"; errors.intro_video = gettext("Key should only contain letters, numbers, _, or -");
} }
// TODO check if key points to a real video using google's youtube api // TODO check if key points to a real video using google's youtube api
} }
......
...@@ -79,14 +79,14 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ ...@@ -79,14 +79,14 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
// FIXME somehow this.collection is unbound sometimes. I can't track down when // FIXME somehow this.collection is unbound sometimes. I can't track down when
var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this); var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this);
if (existing) { if (existing) {
errors.type = "There's already another assignment type with this name."; errors.type = gettext("There's already another assignment type with this name.");
} }
} }
} }
if (_.has(attrs, 'weight')) { if (_.has(attrs, 'weight')) {
var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int
if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) { if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) {
errors.weight = "Please enter an integer between 0 and 100."; errors.weight = gettext("Please enter an integer between 0 and 100.");
} }
else { else {
attrs.weight = intWeight; attrs.weight = intWeight;
...@@ -100,18 +100,20 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ ...@@ -100,18 +100,20 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
}} }}
if (_.has(attrs, 'min_count')) { if (_.has(attrs, 'min_count')) {
if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) { if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) {
errors.min_count = "Please enter an integer."; errors.min_count = gettext("Please enter an integer.");
} }
else attrs.min_count = parseInt(attrs.min_count); else attrs.min_count = parseInt(attrs.min_count);
} }
if (_.has(attrs, 'drop_count')) { if (_.has(attrs, 'drop_count')) {
if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) { if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) {
errors.drop_count = "Please enter an integer."; errors.drop_count = gettext("Please enter an integer.");
} }
else attrs.drop_count = parseInt(attrs.drop_count); else attrs.drop_count = parseInt(attrs.drop_count);
} }
if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) { if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) {
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned."; errors.drop_count = _.template(
gettext("Cannot drop more <% attrs.types %> than will assigned."),
attrs, {variable: 'attrs'});
} }
if (!_.isEmpty(errors)) return errors; if (!_.isEmpty(errors)) return errors;
} }
......
...@@ -9,7 +9,7 @@ function removeAsset(e){ ...@@ -9,7 +9,7 @@ function removeAsset(e){
e.preventDefault(); e.preventDefault();
var that = this; var that = this;
var msg = new CMS.Views.Prompt.Confirmation({ var msg = new CMS.Views.Prompt.Warning({
title: gettext("Delete File Confirmation"), title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"), message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: { actions: {
......
...@@ -26,8 +26,8 @@ CMS.Views.ShowTextbook = Backbone.View.extend({ ...@@ -26,8 +26,8 @@ CMS.Views.ShowTextbook = Backbone.View.extend({
if(e && e.preventDefault) { e.preventDefault(); } if(e && e.preventDefault) { e.preventDefault(); }
var textbook = this.model, collection = this.model.collection; var textbook = this.model, collection = this.model.collection;
var msg = new CMS.Views.Prompt.Warning({ var msg = new CMS.Views.Prompt.Warning({
title: _.str.sprintf(gettext("Delete “%s”?"), title: _.template(gettext("Delete “<%= name %>”?"),
textbook.escape('name')), {name: textbook.escape('name')}),
message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."), message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."),
actions: { actions: {
primary: { primary: {
...@@ -241,8 +241,8 @@ CMS.Views.EditChapter = Backbone.View.extend({ ...@@ -241,8 +241,8 @@ CMS.Views.EditChapter = Backbone.View.extend({
asset_path: this.$("input.chapter-asset-path").val() asset_path: this.$("input.chapter-asset-path").val()
}); });
var msg = new CMS.Models.FileUpload({ var msg = new CMS.Models.FileUpload({
title: _.str.sprintf(gettext("Upload a new asset to %s"), title: _.template(gettext("Upload a new asset to “<%= name %>”"),
section.escape('name')), {name: section.escape('name')}),
message: "Files must be in PDF format." message: "Files must be in PDF format."
}); });
var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model}); var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model});
...@@ -260,7 +260,7 @@ CMS.Views.UploadDialog = Backbone.View.extend({ ...@@ -260,7 +260,7 @@ CMS.Views.UploadDialog = Backbone.View.extend({
this.listenTo(this.model, "change", this.render); this.listenTo(this.model, "change", this.render);
}, },
render: function() { render: function() {
var isValid = this.model.isValid() var isValid = this.model.isValid();
var selectedFile = this.model.get('selectedFile'); var selectedFile = this.model.get('selectedFile');
var oldInput = this.$("input[type=file]").get(0); var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({ this.$el.html(this.template({
......
...@@ -558,7 +558,7 @@ p, ul, ol, dl { ...@@ -558,7 +558,7 @@ p, ul, ol, dl {
// misc // misc
hr.divide { hr.divide {
@extend .text-sr; @extend .cont-text-sr;
} }
.item-details { .item-details {
...@@ -806,7 +806,7 @@ hr.divide { ...@@ -806,7 +806,7 @@ hr.divide {
// basic utility // basic utility
.sr { .sr {
@extend .text-sr; @extend .cont-text-sr;
} }
.fake-link { .fake-link {
...@@ -859,7 +859,7 @@ body.js { ...@@ -859,7 +859,7 @@ body.js {
text-align: center; text-align: center;
.label { .label {
@extend .text-sr; @extend .cont-text-sr;
} }
[class^="icon-"] { [class^="icon-"] {
......
../../../common/static/sass/_mixins-inherited.scss
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// gray primary button // gray primary button
.btn-primary-gray { .btn-primary-gray {
@extend .btn-primary; @extend .ui-btn-primary;
background: $gray-l1; background: $gray-l1;
border-color: $gray-l2; border-color: $gray-l2;
color: $white; color: $white;
...@@ -25,14 +25,14 @@ ...@@ -25,14 +25,14 @@
// blue primary button // blue primary button
.btn-primary-blue { .btn-primary-blue {
@extend .btn-primary; @extend .ui-btn-primary;
background: $blue-u1; background: $blue;
border-color: $blue-u1; border-color: $blue-s1;
color: $white; color: $white;
&:hover, &:active { &:hover, &:active {
background: $blue-s1; background: $blue-s2;
border-color: $blue-s1; border-color: $blue-s2;
} }
&.current, &.active { &.current, &.active {
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
// green primary button // green primary button
.btn-primary-green { .btn-primary-green {
@extend .btn-primary; @extend .ui-btn-primary;
background: $green; background: $green;
border-color: $green; border-color: $green;
color: $white; color: $white;
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
// gray secondary button // gray secondary button
.btn-secondary-gray { .btn-secondary-gray {
@extend .btn-secondary; @extend .ui-btn-secondary;
border-color: $gray-l3; border-color: $gray-l3;
color: $gray-l1; color: $gray-l1;
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
// blue secondary button // blue secondary button
.btn-secondary-blue { .btn-secondary-blue {
@extend .btn-secondary; @extend .ui-btn-secondary;
border-color: $blue-l3; border-color: $blue-l3;
color: $blue; color: $blue;
...@@ -114,7 +114,7 @@ ...@@ -114,7 +114,7 @@
// green secondary button // green secondary button
.btn-secondary-green { .btn-secondary-green {
@extend .btn-secondary; @extend .ui-btn-secondary;
border-color: $green-l4; border-color: $green-l4;
color: $green-l2; color: $green-l2;
...@@ -148,9 +148,9 @@ ...@@ -148,9 +148,9 @@
// ==================== // ====================
// simple dropdown button styling - should we move this elsewhere? // simple dropdown button styling - should we move this elsewhere?
.btn-dd { .ui-btn-dd {
@extend .btn; @extend .ui-btn;
@extend .btn-pill; @extend .ui-btn-pill;
padding:($baseline/4) ($baseline/2); padding:($baseline/4) ($baseline/2);
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
text-align: center; text-align: center;
&:hover, &:active { &:hover, &:active {
@extend .fake-link; @extend .ui-fake-link;
border-color: $gray-l3; border-color: $gray-l3;
} }
...@@ -169,8 +169,8 @@ ...@@ -169,8 +169,8 @@
} }
// layout-based buttons - nav dd // layout-based buttons - nav dd
.btn-dd-nav-primary { .ui-btn-dd-nav-primary {
@extend .btn-dd; @extend .ui-btn-dd;
background: $white; background: $white;
border-color: $white; border-color: $white;
color: $gray-d1; color: $gray-d1;
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// ==================== // ====================
.wrapper-header { .wrapper-header {
@extend .depth3; @extend .ui-depth3;
box-shadow: 0 1px 2px 0 $shadow-l1;
position: relative; position: relative;
width: 100%; width: 100%;
box-shadow: 0 1px 2px 0 $shadow-l1;
margin: 0; margin: 0;
padding: 0 $baseline; padding: 0 $baseline;
background: $white; background: $white;
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
// ==================== // ====================
// basic layout // basic layout
.wrapper-l, .wrapper-r { .wrapper-l, .wrapper-r {
background: $white; background: $white;
} }
...@@ -76,7 +75,7 @@ ...@@ -76,7 +75,7 @@
.title { .title {
@extend .t-action2; @extend .t-action2;
@extend .btn-dd-nav-primary; @extend .ui-btn-dd-nav-primary;
@include transition(all $tmg-f2 ease-in-out 0s); @include transition(all $tmg-f2 ease-in-out 0s);
.label, .icon-caret-down { .label, .icon-caret-down {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
.modal-cover { .modal-cover {
@extend .depth3; @extend .ui-depth3;
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
} }
.modal { .modal {
@extend .depth4; @extend .ui-depth4;
display: none; display: none;
position: fixed; position: fixed;
top: 60px; top: 60px;
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
// lean modal alternative // lean modal alternative
#lean_overlay { #lean_overlay {
@extend .depth4; @extend .ui-depth4;
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
nav { nav {
ol, ul { ol, ul {
@extend .no-list; @extend .cont-no-list;
} }
.nav-item { .nav-item {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.wrapper-inner { .wrapper-inner {
@include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%); @include linear-gradient($gray-d3 0%, $gray-d3 98%, $black 100%);
@extend .depth0; @extend .ui-depth0;
display: none; display: none;
width: 100% !important; width: 100% !important;
border-bottom: 1px solid $white; border-bottom: 1px solid $white;
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
// sock - actions // sock - actions
.list-cta { .list-cta {
@extend .depth1; @extend .ui-depth1;
position: absolute; position: absolute;
top: -($baseline*0.75); top: -($baseline*0.75);
width: 100%; width: 100%;
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
text-align: center; text-align: center;
.cta-show-sock { .cta-show-sock {
@extend .btn-pill; @extend .ui-btn-pill;
@extend .t-action4; @extend .t-action4;
background: $gray-l5; background: $gray-l5;
padding: ($baseline/2) $baseline; padding: ($baseline/2) $baseline;
......
...@@ -186,8 +186,8 @@ ...@@ -186,8 +186,8 @@
// prompts // prompts
.wrapper-prompt { .wrapper-prompt {
@extend .depth5; @extend .ui-depth5;
@include transition(all $tmg-f3 ease-in-out 0s); @include transition(all $tmg-f3 ease-in-out 0s);
position: fixed; position: fixed;
top: 0; top: 0;
background: $black-t0; background: $black-t0;
...@@ -284,7 +284,7 @@ ...@@ -284,7 +284,7 @@
// notifications // notifications
.wrapper-notification { .wrapper-notification {
@extend .depth5; @extend .ui-depth5;
@include clearfix(); @include clearfix();
box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue; box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $blue;
position: fixed; position: fixed;
...@@ -486,7 +486,7 @@ ...@@ -486,7 +486,7 @@
} }
.copy p { .copy p {
@extend .text-sr; @extend .cont-text-sr;
} }
} }
} }
...@@ -495,7 +495,7 @@ ...@@ -495,7 +495,7 @@
// alerts // alerts
.wrapper-alert { .wrapper-alert {
@extend .depth2; @extend .ui-depth2;
@include box-sizing(border-box); @include box-sizing(border-box);
box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue; box-shadow: 0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue;
position: relative; position: relative;
...@@ -641,7 +641,7 @@ ...@@ -641,7 +641,7 @@
text-align: center; text-align: center;
.label { .label {
@extend .text-sr; @extend .cont-text-sr;
} }
[class^="icon"] { [class^="icon"] {
...@@ -738,7 +738,7 @@ body.uxdesign.alerts { ...@@ -738,7 +738,7 @@ body.uxdesign.alerts {
} }
.content-primary { .content-primary {
@extend .window; @extend .ui-window;
width: flex-grid(12, 12); width: flex-grid(12, 12);
margin-right: flex-gutter(); margin-right: flex-gutter();
padding: $baseline ($baseline*1.5); padding: $baseline ($baseline*1.5);
......
...@@ -14,7 +14,7 @@ body.course.checklists { ...@@ -14,7 +14,7 @@ body.course.checklists {
// checklists - general // checklists - general
.course-checklist { .course-checklist {
@extend .window; @extend .ui-window;
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
&:last-child { &:last-child {
...@@ -23,7 +23,7 @@ body.course.checklists { ...@@ -23,7 +23,7 @@ body.course.checklists {
// visual status // visual status
.viz-checklist-status { .viz-checklist-status {
@extend .text-hide; @extend .cont-text-hide;
@include size(100%,($baseline/4)); @include size(100%,($baseline/4));
position: relative; position: relative;
display: block; display: block;
...@@ -40,7 +40,7 @@ body.course.checklists { ...@@ -40,7 +40,7 @@ body.course.checklists {
background: $green; background: $green;
.int { .int {
@extend .text-sr; @extend .cont-text-sr;
} }
} }
} }
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// ==================== // ====================
body.course.export { body.course.export {
.export-overview { .export-overview {
@extend .window; @extend .ui-window;
@include clearfix; @include clearfix;
padding: 30px 40px; padding: 30px 40px;
} }
...@@ -40,7 +40,7 @@ body.course.export { ...@@ -40,7 +40,7 @@ body.course.export {
} }
.export-form-wrapper { .export-form-wrapper {
.export-form { .export-form {
float: left; float: left;
width: 35%; width: 35%;
...@@ -122,4 +122,4 @@ body.course.export { ...@@ -122,4 +122,4 @@ body.course.export {
} }
} }
} }
} }
\ No newline at end of file
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// ==================== // ====================
body.course.import { body.course.import {
.import-overview { .import-overview {
@extend .window; @extend .ui-window;
@include clearfix; @include clearfix;
padding: 30px 40px; padding: 30px 40px;
} }
...@@ -103,4 +103,4 @@ body.course.import { ...@@ -103,4 +103,4 @@ body.course.import {
color: #fff; color: #fff;
line-height: 48px; line-height: 48px;
} }
} }
\ No newline at end of file
...@@ -9,7 +9,7 @@ body.course.settings { ...@@ -9,7 +9,7 @@ body.course.settings {
} }
.content-primary { .content-primary {
@extend .window; @extend .ui-window;
width: flex-grid(9, 12); width: flex-grid(9, 12);
margin-right: flex-gutter(); margin-right: flex-gutter();
padding: $baseline ($baseline*1.5); padding: $baseline ($baseline*1.5);
......
...@@ -171,7 +171,7 @@ body.course.static-pages { ...@@ -171,7 +171,7 @@ body.course.static-pages {
} }
.static-page-details { .static-page-details {
@extend .window; @extend .ui-window;
padding: 32px 40px; padding: 32px 40px;
.row { .row {
......
...@@ -115,7 +115,7 @@ body.course.textbooks { ...@@ -115,7 +115,7 @@ body.course.textbooks {
} }
.delete { .delete {
@extend .btn-non; @extend .ui-btn-non;
} }
} }
...@@ -188,7 +188,7 @@ body.course.textbooks { ...@@ -188,7 +188,7 @@ body.course.textbooks {
.chapters-fields, .chapters-fields,
.textbook-fields { .textbook-fields {
@extend .no-list; @extend .cont-no-list;
.field { .field {
margin: 0 0 ($baseline*0.75) 0; margin: 0 0 ($baseline*0.75) 0;
...@@ -320,7 +320,7 @@ body.course.textbooks { ...@@ -320,7 +320,7 @@ body.course.textbooks {
} }
.action-upload { .action-upload {
@extend .btn-flat-outline; @extend .ui-btn-flat-outline;
position: absolute; position: absolute;
top: 3px; top: 3px;
right: 0; right: 0;
...@@ -348,7 +348,7 @@ body.course.textbooks { ...@@ -348,7 +348,7 @@ body.course.textbooks {
.action-add-chapter { .action-add-chapter {
@extend .btn-flat-outline; @extend .ui-btn-flat-outline;
@include font-size(16); @include font-size(16);
display: block; display: block;
width: 100%; width: 100%;
...@@ -365,7 +365,7 @@ body.course.textbooks { ...@@ -365,7 +365,7 @@ body.course.textbooks {
// dialog // dialog
.wrapper-dialog { .wrapper-dialog {
@extend .depth5; @extend .ui-depth5;
@include transition(all 0.05s ease-in-out); @include transition(all 0.05s ease-in-out);
position: fixed; position: fixed;
top: 0; top: 0;
......
...@@ -2,12 +2,6 @@ ...@@ -2,12 +2,6 @@
// ==================== // ====================
body.course.updates { body.course.updates {
h2 {
margin-bottom: 24px;
font-size: 22px;
font-weight: 300;
}
.course-info-wrapper { .course-info-wrapper {
display: table; display: table;
...@@ -180,9 +174,10 @@ body.course.updates { ...@@ -180,9 +174,10 @@ body.course.updates {
border-left: none; border-left: none;
background: $lightGrey; background: $lightGrey;
h2 { .title {
font-size: 18px; margin-bottom: 24px;
font-weight: 700; font-size: 22px;
font-weight: 300;
} }
.edit-button { .edit-button {
...@@ -220,4 +215,4 @@ body.course.updates { ...@@ -220,4 +215,4 @@ body.course.updates {
textarea { textarea {
height: 300px; height: 300px;
} }
} }
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Page Not Found</%block> <%block name="title">${_("Page Not Found")}</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<h1>Page not found</h1> <h1>${_("Page not found")}</h1>
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p> <p>${_('The page that you were looking for was not found.')}
${_('Go back to the {homepage} or let us know about any pages that may have been moved at {email}.').format(
homepage='<a href="/">homepage</a>',
email='<a href="mailto:technical@edx.org">technical@edx.org</a>')}
</p>
</section> </section>
</div> </div>
</%block> </%block>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Studio Server Error</%block> <%block name="title">${_("Studio Server Error")}</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<h1>The <em>Studio</em> servers encountered an error</h1> <h1>${_("The <em>Studio</em> servers encountered an error")}</h1>
<p> <p>
An error occurred in Studio and the page could not be loaded. Please try again in a few moments. ${_("An error occurred in Studio and the page could not be loaded. Please try again in a few moments.")}
We've logged the error and our staff is currently working to resolve this error as soon as possible. ${_("We've logged the error and our staff is currently working to resolve this error as soon as possible.")}
If the problem persists, please email us at <a href="mailto:technical@edx.org">technical@edx.org</a>. ${_('If the problem persists, please email us at {email}.').format(email='<a href="mailto:technical@edx.org">technical@edx.org</a>')}
</p> </p>
</section> </section>
</div> </div>
</%block> </%block>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
...@@ -29,5 +30,3 @@ ...@@ -29,5 +30,3 @@
</section> </section>
</div> </div>
</%block> </%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="content"> <%block name="content">
...@@ -30,6 +31,3 @@ ...@@ -30,6 +31,3 @@
</section> </section>
</div> </div>
</%block> </%block>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%block name="bodyclass">is-signedin course uploads</%block> <%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Files &amp; Uploads</%block> <%block name="title">${_("Files &amp; Uploads")}</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> Upload New File</a> <a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
......
## -*- coding: utf-8 -*-
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<!doctype html> <!doctype html>
...@@ -17,6 +18,7 @@ ...@@ -17,6 +18,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${MITX_ROOT_URL}"> <meta name="path_prefix" content="${MITX_ROOT_URL}">
<script type="text/javascript" src="/jsi18n/"></script>
<%static:css group='base-style'/> <%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
...@@ -35,7 +37,6 @@ ...@@ -35,7 +37,6 @@
</script> </script>
## javascript ## javascript
<script type="text/javascript" src="/jsi18n/"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore.string.min.js')}"></script>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Checklists</%block> <%block name="title">Course Checklists</%block>
...@@ -30,8 +31,8 @@ ...@@ -30,8 +31,8 @@
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Tools</small> <small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>Course Checklists <span class="sr">&gt; </span>${_("Course Checklists")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -40,18 +41,18 @@ ...@@ -40,18 +41,18 @@
<section class="content"> <section class="content">
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action=""> <form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">Current Checklists</h2> <h2 class="title title-3 sr">${_("Current Checklists")}</h2>
</form> </form>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title title-3">What are checklists?</h3> <h3 class="title title-3">${_("What are checklists?")}</h3>
<p> <p>
Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students. ${_("Running a course on edX is a complex undertaking. Course checklists are designed to help you understand and keep track of all the steps necessary to get your course ready for students.")}
</p> </p>
<p> <p>
These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically. ${_("These checklists are shared among your course team, and any changes you make are immediately visible to other members of the team and saved automatically.")}
</p> </p>
</div> </div>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title --> <!-- TODO decode course # from context_course into title -->
<%block name="title">Course Updates</%block> <%block name="title">${_("Course Updates")}</%block>
<%block name="bodyclass">is-signedin course course-info updates</%block> <%block name="bodyclass">is-signedin course course-info updates</%block>
...@@ -44,15 +45,15 @@ ...@@ -44,15 +45,15 @@
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Content</small> <small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>Course Updates <span class="sr">&gt; </span>${_("Course Updates")}
</h1> </h1>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">${_('Page Actions')}</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class=" button new-button new-update-button"><i class="icon-plus"></i> New Update</a> <a href="#" class=" button new-button new-update-button"><i class="icon-plus"></i> ${_('New Update')}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -62,7 +63,7 @@ ...@@ -62,7 +63,7 @@
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<div class="introduction"> <div class="introduction">
<p clas="copy">Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.</p> <p clas="copy">${_('Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.')}</p>
</div> </div>
</section> </section>
</div> </div>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Static Pages</%block> <%block name="title">Static Pages</%block>
<%block name="bodyclass">is-signedin course pages static-pages</%block> <%block name="bodyclass">is-signedin course pages static-pages</%block>
...@@ -19,15 +20,15 @@ ...@@ -19,15 +20,15 @@
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Content</small> <small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>Static Pages <span class="sr">&gt; </span>${_("Static Pages")}
</h1> </h1>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">${_("Page Actions")}</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button new-button new-tab"><i class="icon-plus"></i> New Page</a> <a href="#" class="button new-button new-tab"><i class="icon-plus"></i> ${_("New Page")}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -37,11 +38,11 @@ ...@@ -37,11 +38,11 @@
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<div class="introduction has-links"> <div class="introduction has-links">
<p class="copy">Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.</p> <p class="copy">${_("Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.")}</p>
<nav class="nav-introduction-supplementary"> <nav class="nav-introduction-supplementary">
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a rel="modal" href="#preview-lms-staticpages"><i class="icon-question-sign"></i>How do Static Pages look to students in my course?</a> <a rel="modal" href="#preview-lms-staticpages"><i class="icon-question-sign"></i>${_("How do Static Pages look to students in my course?")}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
...@@ -69,15 +70,15 @@ ...@@ -69,15 +70,15 @@
</div> </div>
<div class="content-modal" id="preview-lms-staticpages"> <div class="content-modal" id="preview-lms-staticpages">
<h3 class="title">How Static Pages are Used in Your Course</h3> <h3 class="title">${_("How Static Pages are Used in Your Course")}</h3>
<figure> <figure>
<img src="/static/img/preview-lms-staticpages.png" alt="Preview of how Static Pages are used in your course" /> <img src="/static/img/preview-lms-staticpages.png" alt="${_('Preview of how Static Pages are used in your course')}" />
<figcaption class="description">These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.</figcaption> <figcaption class="description">${_("These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.")}</figcaption>
</figure> </figure>
<a href="#" rel="view" class="action action-modal-close"> <a href="#" rel="view" class="action action-modal-close">
<i class="icon-remove-sign"></i> <i class="icon-remove-sign"></i>
<span class="label">close modal</span> <span class="label">${_("close modal")}</span>
</a> </a>
</div> </div>
</%block> </%block>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
import logging import logging
...@@ -5,7 +6,7 @@ ...@@ -5,7 +6,7 @@
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">CMS Subsection</%block> <%block name="title">${_("CMS Subsection")}</%block>
<%block name="bodyclass">is-signedin course subsection</%block> <%block name="bodyclass">is-signedin course subsection</%block>
...@@ -18,11 +19,11 @@ ...@@ -18,11 +19,11 @@
<div class="main-column"> <div class="main-column">
<article class="subsection-body window" data-id="${subsection.location}"> <article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input"> <div class="subsection-name-input">
<label>Display Name:</label> <label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/> <input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div> </div>
<div class="sortable-unit-list"> <div class="sortable-unit-list">
<label>Units:</label> <label>${_("Units:")}</label>
${units.enum_units(subsection, subsection_units=subsection_units)} ${units.enum_units(subsection, subsection_units=subsection_units)}
</div> </div>
</article> </article>
...@@ -30,63 +31,61 @@ ...@@ -30,63 +31,61 @@
<div class="sidebar"> <div class="sidebar">
<div class="unit-settings window id-holder" data-id="${subsection.location}"> <div class="unit-settings window id-holder" data-id="${subsection.location}">
<h4 class="header">Subsection Settings</h4> <h4 class="header">${_("Subsection Settings")}</h4>
<div class="window-contents"> <div class="window-contents">
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
<div class="datepair" data-language="javascript"> <div class="datepair" data-language="javascript">
<div class="field field-start-date"> <div class="field field-start-date">
<label for="start_date">Release Day</label> <label for="start_date">${_("Release Day")}</label>
<input type="text" id="start_date" name="start_date" <input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}" value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="start_time">Release Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label> <label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label>
<input type="text" id="start_time" name="start_time" <input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}" value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
</div> </div>
% if subsection.lms.start and not almost_same_datetime(subsection.lms.start, parent_item.lms.start): % if subsection.lms.start and not almost_same_datetime(subsection.lms.start, parent_item.lms.start):
% if parent_item.lms.start is None: % if parent_item.lms.start is None:
<p class="notice">The date above differs from the release date of <p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)}
${parent_item.display_name_with_default}, which is unset.
% else: % else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – <p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.lms.start))}.
${get_default_time_display(parent_item.lms.start)}.
% endif % endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p> <a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p>
% endif % endif
</div> </div>
<div class="row gradable"> <div class="row gradable">
<label>Graded as:</label> <label>${_("Graded as:")}</label>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}"> <div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}">
</div> </div>
<div class="due-date-input row"> <div class="due-date-input row">
<a href="#" class="set-date">Set a due date</a> <a href="#" class="set-date">${_("Set a due date")}</a>
<div class="datepair date-setter"> <div class="datepair date-setter">
<div class="field field-start-date"> <div class="field field-start-date">
<label for="due_date">Due Day</label> <label for="due_date">${_("Due Day")}</label>
<input type="text" id="due_date" name="due_date" <input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}" value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="due_time">Due Time (<abbr title="Coordinated Universal Time">UTC</abbr>)</label> <label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time" <input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}" value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
<a href="#" class="remove-date">Remove due date</a> <a href="#" class="remove-date">${_("Remove due date")}</a>
</div> </div>
</div> </div>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="${preview_link}" target="_blank" class="preview-button">Preview Drafts</a> <a href="${preview_link}" target="_blank" class="preview-button">${_("Preview Drafts")}</a>
%if can_view_live: %if can_view_live:
<a href="${lms_link}" target="_blank" class="preview-button">View Live</a> <a href="${lms_link}" target="_blank" class="preview-button">${_("View Live")}</a>
%endif %endif
</div> </div>
</div> </div>
...@@ -119,19 +118,19 @@ ...@@ -119,19 +118,19 @@
// TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars // TODO figure out whether these should be in window or someplace else or whether they're only needed as local vars
// I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally // I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally
// but we really should change that behavior. // but we really should change that behavior.
if (!window.graderTypes) { if (!window.graderTypes) {
window.graderTypes = new CMS.Models.Settings.CourseGraderCollection(); window.graderTypes = new CMS.Models.Settings.CourseGraderCollection();
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}'); window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n}); window.graderTypes.reset(${course_graders|n});
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({ var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele, el : ele,
graders : window.graderTypes, graders : window.graderTypes,
hideSymbol : true hideSymbol : true
}); });
}); });
}) })
</script> </script>
......
Thank you for signing up for edX Studio! To activate your account, <%! from django.utils.translation import ugettext as _ %>
please copy and paste this address into your web browser's
address bar: ${_("Thank you for signing up for edX Studio! To activate your account, please copy and paste this address into your web browser's address bar:")}
% if is_secure: % if is_secure:
https://${ site }/activate/${ key } https://${ site }/activate/${ key }
...@@ -8,6 +8,4 @@ address bar: ...@@ -8,6 +8,4 @@ address bar:
http://${ site }/activate/${ key } http://${ site }/activate/${ key }
% endif % endif
If you didn't request this, you don't need to do anything; you won't ${_("If you didn't request this, you don't need to do anything; you won't receive any more email from us. Please do not reply to this e-mail; if you require assistance, check the help section of the edX web site.")}
receive any more email from us. Please do not reply to this e-mail; if
you require assistance, check the help section of the edX web site.
Your account for edX Studio <%! from django.utils.translation import ugettext as _ %>
${_("Your account for edX Studio")}
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">error</%block> <%block name="bodyclass">error</%block>
<%block name="title"> <%block name="title">
% if error == '404': % if error == '404':
404 - Page Not Found 404 - ${_("Page Not Found")}
% elif error == '500': % elif error == '500':
500 - Internal Server Error 500 - ${_("Internal Server Error")}
% endif % endif
</%block> </%block>
<%block name="content"> <%block name="content">
<article class="error-prompt"> <article class="error-prompt">
% if error == '404': % if error == '404':
<h1>Hmm…</h1> <h1>${_("The Page You Requested Page Cannot be Found")}</h1>
<p class="description">we can't find that page.</p> <p class="description">${_("We're sorry. We couldn't find the Studio page you're looking for. You may want to return to the Studio Dashboard and try again. If you are still having problems accessing things, please feel free to {link_start}contact Studio support{link_end} for further help.").format(
% elif error == '500': link_start='<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="{title}">'.format(title=_("Use our feedback tool, Tender, to share your feedback")),
<h1>Oops…</h1> link_end='</a>',
<p class="description">there was a problem with the server.</p> )}</p>
% endif % elif error == '500':
<a href="/" class="back-button">Back to dashboard</a> <h1>${_("The Server Encountered an Error")}</h1>
<p class="description">${_("We're sorry. There was a problem with the server while trying to process your last request. You may want to return to the Studio Dashboard or try this request again. If you are still having problems accessing things, please feel free to {link_start}contact Studio support{link_end} for further help.").format(
link_start='<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="{title}">'.format(title=_("Use our feedback tool, Tender, to share your feedback")),
link_end='</a>',
)}</p>
% endif
<a href="/" class="back-button">${_("Back to dashboard")}</a>
</article> </article>
</%block> </%block>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Export</%block> <%block name="title">${_("Course Export")}</%block>
<%block name="bodyclass">is-signedin course tools export</%block> <%block name="bodyclass">is-signedin course tools export</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-subtitle"> <header class="mast has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Tools</small> <small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>Course Export <span class="sr">&gt; </span>${_("Course Export")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -19,28 +20,29 @@ ...@@ -19,28 +20,29 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<article class="export-overview"> <article class="export-overview">
<div class="description"> <div class="description">
<h2>About Exporting Courses</h2> <h2>${_("About Exporting Courses")}</h2>
<p>When exporting your course, you will receive a .tar.gz formatted file that contains the following course data:</p> ## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("When exporting your course, you will receive a .tar.gz formatted file that contains the following course data:")}</p>
<ul> <ul>
<li>Course Structure (Sections and sub-section ordering)</li> <li>${_("Course Structure (Sections and sub-section ordering)")}</li>
<li>Individual Units</li> <li>${_("Individual Units")}</li>
<li>Individual Problems</li> <li>${_("Individual Problems")}</li>
<li>Static Pages</li> <li>${_("Static Pages")}</li>
<li>Course Assets</li> <li>${_("Course Assets")}</li>
</ul> </ul>
<p>Your course export <strong>will not include</strong>: student data, forum/discussion data, course settings, certificates, grading information, or user data.</p> <p>${_("Your course export <strong>will not include</strong>: student data, forum/discussion data, course settings, certificates, grading information, or user data.")}</p>
</div> </div>
<!-- default state --> <!-- default state -->
<div class="export-form-wrapper"> <div class="export-form-wrapper">
<form action="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form"> <form action="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2> <h2>${_("Export Course:")}</h2>
<p class="error-block"></p> <p class="error-block"></p>
<a href="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" class="button-export">Download Files</a> <a href="${reverse('generate_export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" class="button-export">${_("Download Files")}</a>
</form> </form>
</div> </div>
...@@ -48,12 +50,12 @@ ...@@ -48,12 +50,12 @@
<%doc> <%doc>
<div class="export-form-wrapper is-downloading"> <div class="export-form-wrapper is-downloading">
<form action="${reverse('export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form"> <form action="${reverse('export_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="export-form">
<h2>Export Course:</h2> <h2>${_("Export Course:")}</h2>
<p class="error-block"></p> <p class="error-block"></p>
<a href="#" class="button-export disabled">Files Downloading</a> <a href="#" class="button-export disabled">Files Downloading</a>
<p class="message-status">Download not start? <a href="#" class="text-export">Try again</a></p> <p class="message-status">${_("Download not start?")} <a href="#" class="text-export">${_("Try again")}</a></p>
</form> </form>
</div> </div>
</%doc> </%doc>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Import</%block> <%block name="title">${_("Course Import")}</%block>
<%block name="bodyclass">is-signedin course tools import</%block> <%block name="bodyclass">is-signedin course tools import</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-subtitle"> <header class="mast has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Tools</small> <small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>Course Import <span class="sr">&gt; </span>${_("Course Import")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -19,19 +20,18 @@ ...@@ -19,19 +20,18 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<article class="import-overview"> <article class="import-overview">
<div class="description"> <div class="description">
<p><strong>Importing a new course will delete all content currently associated with your course <p><strong>${_("Importing a new course will delete all content currently associated with your course and replace it with the contents of the uploaded file.")}</strong></p>
and replace it with the contents of the uploaded file.</strong></p> ## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
<p>File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a <code>course.xml</code> file.</p> <p>${_("File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a {filename} file.").format(filename='<code>course.xml</code>')}</p>
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes, <p>${_("Please note that if your course has any problems with auto-generated {nodename} nodes, re-importing your course could cause the loss of student data associated with those problems.").format(nodename='<code>url_name</code>')}</p>
re-importing your course could cause the loss of student data associated with those problems.</p>
</div> </div>
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form"> <form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
<h2>Course to import:</h2> <h2>${_("Course to import:")}</h2>
<p class="error-block"></p> <p class="error-block"></p>
<a href="#" class="choose-file-button">Choose File</a> <a href="#" class="choose-file-button">${_("Choose File")}</a>
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p> <p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">${_("change")}</a></p>
<input type="file" name="course-data" class="file-input"> <input type="file" name="course-data" class="file-input">
<input type="submit" value="Replace my course with the one above" class="submit-button"> <input type="submit" value="${_('Replace my course with the one above')}" class="submit-button">
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill"></div> <div class="progress-fill"></div>
<div class="percent">0%</div> <div class="percent">0%</div>
...@@ -68,11 +68,11 @@ $('form').ajaxForm({ ...@@ -68,11 +68,11 @@ $('form').ajaxForm({
}, },
complete: function(xhr) { complete: function(xhr) {
if (xhr.status == 200) { if (xhr.status == 200) {
alert('Your import was successful.'); alert('${_("Your import was successful.")}');
window.location = '${successful_import_redirect_url}'; window.location = '${successful_import_redirect_url}';
} }
else else
alert('Your import has failed.\n\n' + xhr.responseText); alert('${_("Your import has failed.")}\n\n' + xhr.responseText);
submitBtn.show(); submitBtn.show();
bar.hide(); bar.hide();
} }
......
...@@ -36,22 +36,22 @@ ...@@ -36,22 +36,22 @@
<div class="item-details"> <div class="item-details">
<form class="course-info"> <form class="course-info">
<div class="row"> <div class="row">
<label>Course Name</label> <label>${_("Course Name")}</label>
<input type="text" class="new-course-name" /> <input type="text" class="new-course-name" />
</div> </div>
<div class="row"> <div class="row">
<div class="column"> <div class="column">
<label>Organization</label> <label>${_("Organization")}</label>
<input type="text" class="new-course-org" /> <input type="text" class="new-course-org" />
</div> </div>
<div class="column"> <div class="column">
<label>Course Number</label> <label>${_("Course Number")}</label>
<input type="text" class="new-course-number" /> <input type="text" class="new-course-number" />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<input type="submit" value="Save" class="new-course-save" data-template="${new_course_template}" /> <input type="submit" value="${_('Save')}" class="new-course-save"/>
<input type="button" value="Cancel" class="new-course-cancel" /> <input type="button" value="${_('Cancel')}" class="new-course-cancel" />
</div> </div>
</form> </form>
</div> </div>
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
<a class="class-link" href="${url}" class="class-name"> <a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span> <span class="class-name">${course}</span>
</a> </a>
<a href="${lms_link}" rel="external" class="button view-button view-live-button">View Live</a> <a href="${lms_link}" rel="external" class="button view-button view-live-button">${_("View Live")}</a>
</li> </li>
%endfor %endfor
</ul> </ul>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Sign In</%block> <%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin signin</%block> <%block name="bodyclass">not-signedin signin</%block>
<%block name="content"> <%block name="content">
...@@ -8,32 +9,32 @@ ...@@ -8,32 +9,32 @@
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<header> <header>
<h1 class="title title-1">Sign In to edX Studio</h1> <h1 class="title title-1">${_("Sign In to edX Studio")}</h1>
<a href="${reverse('signup')}" class="action action-signin">Don't have a Studio Account? Sign up!</a> <a href="${reverse('signup')}" class="action action-signin">${_("Don't have a Studio Account? Sign up!")}</a>
</header> </header>
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post"> <form id="login_form" method="post" action="login_post">
<fieldset> <fieldset>
<legend class="sr">Required Information to Sign In to edX Studio</legend> <legend class="sr">${_("Required Information to Sign In to edX Studio")}</legend>
<ol class="list-input"> <ol class="list-input">
<li class="field text required" id="field-email"> <li class="field text required" id="field-email">
<label for="email">Email Address</label> <label for="email">${_("Email Address")}</label>
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" /> <input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
</li> </li>
<li class="field text required" id="field-password"> <li class="field text required" id="field-password">
<a href="${forgot_password_link}" class="action action-forgotpassword" tabindex="-1">Forgot password?</a> <a href="${forgot_password_link}" class="action action-forgotpassword" tabindex="-1">${_("Forgot password?")}</a>
<label for="password">Password</label> <label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" /> <input id="password" type="password" name="password" />
</li> </li>
</ol> </ol>
</fieldset> </fieldset>
<div class="form-actions"> <div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Sign In to edX Studio</button> <button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to edX Studio")}</button>
</div> </div>
<!-- no honor code for CMS, but need it because we're using the lms student object --> <!-- no honor code for CMS, but need it because we're using the lms student object -->
...@@ -42,11 +43,11 @@ ...@@ -42,11 +43,11 @@
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<h2 class="sr">Studio Support</h2> <h2 class="sr">${_("Studio Support")}</h2>
<div class="bit"> <div class="bit">
<h3 class="title-3">Need Help?</h3> <h3 class="title-3">${_("Need Help?")}</h3>
<p>Having trouble with your account? Use <a href="http://help.edge.edx.org" rel="external">our support center</a> to look over self help steps, find solutions others have found to the same problem, or let us know of your issue.</p> <p>${_('Having trouble with your account? Use {link_start}our support center{link_end} to look over self help steps, find solutions others have found to the same problem, or let us know of your issue.').format(link_start='<a href="http://help.edge.edx.org" rel="external">', link_end='</a>')}</p>
</div> </div>
</aside> </aside>
</section> </section>
...@@ -94,4 +95,4 @@ ...@@ -94,4 +95,4 @@
}); });
})(this) })(this)
</script> </script>
</%block> </%block>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Course Team Settings</%block> <%block name="title">${_("Course Team Settings")}</%block>
<%block name="bodyclass">is-signedin course users settings team</%block> <%block name="bodyclass">is-signedin course users settings team</%block>
...@@ -7,16 +8,16 @@ ...@@ -7,16 +8,16 @@
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle"> <header class="mast has-actions has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Course Settings</small> <small class="subtitle">${_("Course Settings")}</small>
<span class="sr">&gt; </span>Course Team <span class="sr">&gt; </span>${_("Course Team")}
</h1> </h1>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">${_("Page Actions")}</h3>
<ul> <ul>
%if allow_actions: %if allow_actions:
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button new-button new-user-button"><i class="icon-plus"></i> New User</a> <a href="#" class="button new-button new-user-button"><i class="icon-plus"></i> ${_("New User")}</a>
</li> </li>
%endif %endif
</ul> </ul>
...@@ -28,7 +29,7 @@ ...@@ -28,7 +29,7 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="details"> <div class="details">
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p> <p>${_("The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.")}</p>
</div> </div>
<article class="user-overview"> <article class="user-overview">
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%inherit file="../base.html" /> <%inherit file="../base.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
%if not user_logged_in: %if not user_logged_in:
<%block name="bodyclass"> <%block name="bodyclass">
not-signedin not-signedin
</%block> </%block>
%endif %endif
...@@ -16,15 +17,15 @@ ...@@ -16,15 +17,15 @@
<p style='padding-top:100px; text-align:center;'> <p style='padding-top:100px; text-align:center;'>
%if not already_active: %if not already_active:
Thanks for activating your account. ${_("Thanks for activating your account.")}
%else: %else:
This account has already been activated. ${_("This account has already been activated.")}
%endif %endif
%if user_logged_in: %if user_logged_in:
Visit your <a href="/">dashboard</a> to see your courses. ${_("Visit your {link_start}dashboard{link_end} to see your courses.").format(link_start='<a href="/">', link_end='</a>')}
%else: %else:
You can now <a href="${reverse('login')}">login</a>. ${_("You can now {link_start}login{link_end}.").format(link_start='<a href="{url}">'.format(url=reverse('login')), link_end='</a>')}
%endif %endif
</p> </p>
</section> </section>
......
<%! from django.utils.translation import ugettext as _ %>
<h1>Check your email</h1> <h1>Check your email</h1>
<p>An activation link has been sent to ${ email }, along with <p>${_("An activation link has been sent to {emaiL}, along with instructions for activating your account.").format(email=email)}</p>
instructions for activating your account.</p>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Schedule &amp; Details Settings</%block> <%block name="title">${_("Schedule &amp; Details Settings")}</%block>
<%block name="bodyclass">is-signedin course schedule settings</%block> <%block name="bodyclass">is-signedin course schedule settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Advanced Settings</%block> <%block name="title">${_("Advanced Settings")}</%block>
<%block name="bodyclass">is-signedin course advanced settings</%block> <%block name="bodyclass">is-signedin course advanced settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
...@@ -44,8 +45,8 @@ editor.render(); ...@@ -44,8 +45,8 @@ editor.render();
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-subtitle"> <header class="mast has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Settings</small> <small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>Advanced Settings <span class="sr">&gt; </span>${_("Advanced Settings")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -56,20 +57,20 @@ editor.render(); ...@@ -56,20 +57,20 @@ editor.render();
<form id="settings_advanced" class="settings-advanced" method="post" action=""> <form id="settings_advanced" class="settings-advanced" method="post" action="">
<div class="message message-status confirm"> <div class="message message-status confirm">
Your policy changes have been saved. ${_("Your policy changes have been saved.")}
</div> </div>
<div class="message message-status error"> <div class="message message-status error">
There was an error saving your information. Please see below. ${_("There was an error saving your information. Please see below.")}
</div> </div>
<section class="group-settings advanced-policies"> <section class="group-settings advanced-policies">
<header> <header>
<h2 class="title-2">Manual Policy Definition</h2> <h2 class="title-2">${_("Manual Policy Definition")}</h2>
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span> <span class="tip">${_("Manually Edit Course Policy Values (JSON Key / Value pairs)")}</span>
</header> </header>
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p> <p class="instructions">${_("<strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.")}</p>
<ul class="list-input course-advanced-policy-list enum"> <ul class="list-input course-advanced-policy-list enum">
...@@ -80,22 +81,22 @@ editor.render(); ...@@ -80,22 +81,22 @@ editor.render();
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title-3">How will these settings be used?</h3> <h3 class="title-3">${_("How will these settings be used?")}</h3>
<p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p> <p>${_("Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.")}</p>
<p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p> <p>${_("Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax)")}.</p>
</div> </div>
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<h3 class="title-3">Other Course Settings</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Grading Settings</%block> <%block name="title">${_("Grading Settings")}</%block>
<%block name="bodyclass">is-signedin course grading settings</%block> <%block name="bodyclass">is-signedin course grading settings</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
...@@ -41,8 +42,8 @@ from contentstore import utils ...@@ -41,8 +42,8 @@ from contentstore import utils
<div class="wrapper-mast wrapper"> <div class="wrapper-mast wrapper">
<header class="mast has-subtitle"> <header class="mast has-subtitle">
<h1 class="page-header"> <h1 class="page-header">
<small class="subtitle">Settings</small> <small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>Grading <span class="sr">&gt; </span>${_("Grading")}
</h1> </h1>
</header> </header>
</div> </div>
...@@ -53,8 +54,8 @@ from contentstore import utils ...@@ -53,8 +54,8 @@ from contentstore import utils
<form id="settings_details" class="settings-grading" method="post" action=""> <form id="settings_details" class="settings-grading" method="post" action="">
<section class="group-settings grade-range"> <section class="group-settings grade-range">
<header> <header>
<h2 class="title-2">Overall Grade Range</h2> <h2 class="title-2">${_("Overall Grade Range")}</h2>
<span class="tip">Your overall grading scale for student final grades</span> <span class="tip">${_("Your overall grading scale for student final grades")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
...@@ -89,15 +90,15 @@ from contentstore import utils ...@@ -89,15 +90,15 @@ from contentstore import utils
<section class="group-settings grade-rules"> <section class="group-settings grade-rules">
<header> <header>
<h2 class="title-2">Grading Rules &amp; Policies</h2> <h2 class="title-2">${_("Grading Rules &amp; Policies")}</h2>
<span class="tip">Deadlines, requirements, and logistics around grading student work</span> <span class="tip">${_("Deadlines, requirements, and logistics around grading student work")}</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
<li class="field text" id="field-course-grading-graceperiod"> <li class="field text" id="field-course-grading-graceperiod">
<label for="course-grading-graceperiod">Grace Period on Deadline:</label> <label for="course-grading-graceperiod">${_("Grace Period on Deadline:")}</label>
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="HH:MM" autocomplete="off" /> <input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="HH:MM" autocomplete="off" />
<span class="tip tip-inline">Leeway on due dates</span> <span class="tip tip-inline">${_("Leeway on due dates")}</span>
</li> </li>
</ol> </ol>
</section> </section>
...@@ -106,8 +107,8 @@ from contentstore import utils ...@@ -106,8 +107,8 @@ from contentstore import utils
<section class="group-settings assignment-types"> <section class="group-settings assignment-types">
<header> <header>
<h2 class="title-2">Assignment Types</h2> <h2 class="title-2">${_("Assignment Types")}</h2>
<span class="tip">Categories and labels for any exercises that are gradable</span> <span class="tip">${_("Categories and labels for any exercises that are gradable")}</span>
</header> </header>
<ol class="list-input course-grading-assignment-list enum"> <ol class="list-input course-grading-assignment-list enum">
...@@ -116,7 +117,7 @@ from contentstore import utils ...@@ -116,7 +117,7 @@ from contentstore import utils
<div class="actions"> <div class="actions">
<a href="#" class="new-button new-course-grading-item add-grading-data"> <a href="#" class="new-button new-course-grading-item add-grading-data">
<i class="icon-plus"></i>New Assignment Type <i class="icon-plus"></i>${_("New Assignment Type")}
</a> </a>
</div> </div>
</section> </section>
...@@ -125,22 +126,22 @@ from contentstore import utils ...@@ -125,22 +126,22 @@ from contentstore import utils
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
<h3 class="title-3">How will these settings be used?</h3> <h3 class="title-3">${_("How will these settings be used?")}</h3>
<p>Your grading settings will be used to calculate students grades and performance.</p> <p>${_("Your grading settings will be used to calculate students grades and performance.")}</p>
<p>Overall grade range will be used in students' final grades, which are calculated by the weighting you determine for each custom assignment type.</p> <p>${_("Overall grade range will be used in students' final grades, which are calculated by the weighting you determine for each custom assignment type.")}</p>
</div> </div>
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<h3 class="title-3">Other Course Settings</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li> <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Sign Up</%block> <%block name="title">${_("Sign Up")}</%block>
<%block name="bodyclass">not-signedin signup</%block> <%block name="bodyclass">not-signedin signup</%block>
<%block name="content"> <%block name="content">
...@@ -9,63 +10,63 @@ ...@@ -9,63 +10,63 @@
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<header> <header>
<h1 class="title title-1">Sign Up for edX Studio</h1> <h1 class="title title-1">${_("Sign Up for edX Studio")}</h1>
<a href="${reverse('login')}" class="action action-signin">Already have a Studio Account? Sign in</a> <a href="${reverse('login')}" class="action action-signin">${_("Already have a Studio Account? Sign in")}</a>
</header> </header>
<p class="introduction">Ready to start creating online courses? Sign up below and start creating your first edX course today.</p> <p class="introduction">${_("Ready to start creating online courses? Sign up below and start creating your first edX course today.")}</p>
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<form id="register_form" method="post" action="register_post"> <form id="register_form" method="post" action="register_post">
<div id="register_error" name="register_error" class="message message-status message-status error"> <div id="register_error" name="register_error" class="message message-status message-status error">
</div> </div>
<fieldset> <fieldset>
<legend class="sr">Required Information to Sign Up for edX Studio</legend> <legend class="sr">${_("Required Information to Sign Up for edX Studio")}</legend>
<ol class="list-input"> <ol class="list-input">
<li class="field text required" id="field-email"> <li class="field text required" id="field-email">
<label for="email">Email Address</label> <label for="email">${_("Email Address")}</label>
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" /> <input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
</li> </li>
<li class="field text required" id="field-password"> <li class="field text required" id="field-password">
<label for="password">Password</label> <label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" /> <input id="password" type="password" name="password" />
</li> </li>
<li class="field text required" id="field-username"> <li class="field text required" id="field-username">
<label for="username">Public Username</label> <label for="username">${_("Public Username")}</label>
<input id="username" type="text" name="username" placeholder="e.g. janedoe" /> <input id="username" type="text" name="username" placeholder="e.g. janedoe" />
<span class="tip tip-stacked">This will be used in public discussions with your courses and in our edX101 support forums</span> <span class="tip tip-stacked">${_("This will be used in public discussions with your courses and in our edX101 support forums")}</span>
</li> </li>
<li class="field text required" id="field-name"> <li class="field text required" id="field-name">
<label for="name">Full Name</label> <label for="name">${_("Full Name")}</label>
<input id="name" type="text" name="name" placeholder="e.g. Jane Doe" /> <input id="name" type="text" name="name" placeholder="e.g. Jane Doe" />
</li> </li>
<li class="field-group"> <li class="field-group">
<div class="field text" id="field-location"> <div class="field text" id="field-location">
<label for="location">Your Location</label> <label for="location">${_("Your Location")}</label>
<input class="short" id="location" type="text" name="location" /> <input class="short" id="location" type="text" name="location" />
</div> </div>
<div class="field text" id="field-language"> <div class="field text" id="field-language">
<label for="language">Preferred Language</label> <label for="language">${_("Preferred Language")}</label>
<input class="short" id="language" type="text" name="language" /> <input class="short" id="language" type="text" name="language" />
</div> </div>
</li> </li>
<li class="field checkbox required" id="field-tos"> <li class="field checkbox required" id="field-tos">
<input id="tos" name="terms_of_service" type="checkbox" value="true" /> <input id="tos" name="terms_of_service" type="checkbox" value="true" />
<label for="tos">I agree to the Terms of Service</label> <label for="tos">${_("I agree to the Terms of Service")}</label>
</li> </li>
</ol> </ol>
</fieldset> </fieldset>
<div class="form-actions"> <div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Create My Account & Start Authoring Courses</button> <button type="submit" id="submit" name="submit" class="action action-primary">${_("Create My Account &amp; Start Authoring Courses")}</button>
</div> </div>
<!-- no honor code for CMS, but need it because we're using the lms student object --> <!-- no honor code for CMS, but need it because we're using the lms student object -->
...@@ -74,21 +75,21 @@ ...@@ -74,21 +75,21 @@
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<h2 class="sr">Common Studio Questions</h2> <h2 class="sr">${_("Common Studio Questions")}</h2>
<div class="bit"> <div class="bit">
<h3 class="title-3">Who is Studio for?</h3> <h3 class="title-3">${_("Who is Studio for?")}</h3>
<p>Studio is for anyone that wants to create online courses that leverage the global edX platform. Our users are often faculty members, teaching assistants and course staff, and members of instructional technology groups.</p> <p>${_("Studio is for anyone that wants to create online courses that leverage the global edX platform. Our users are often faculty members, teaching assistants and course staff, and members of instructional technology groups.")}</p>
</div> </div>
<div class="bit"> <div class="bit">
<h3 class="title-3">How technically savvy do I need to be to create courses in Studio?</h3> <h3 class="title-3">${_("How technically savvy do I need to be to create courses in Studio?")}</h3>
<p>Studio is designed to be easy to use by almost anyone familiar with common web-based authoring environments (Wordpress, Moodle, etc.). No programming knowledge is required, but for some of the more advanced features, a technical background would be helpful. As always, we are here to help, so don't hesitate to dive right in.</p> <p>${_("Studio is designed to be easy to use by almost anyone familiar with common web-based authoring environments (Wordpress, Moodle, etc.). No programming knowledge is required, but for some of the more advanced features, a technical background would be helpful. As always, we are here to help, so don't hesitate to dive right in.")}</p>
</div> </div>
<div class="bit"> <div class="bit">
<h3 class="title-3">I've never authored a course online before. Is there help?</h3> <h3 class="title-3">${_("I've never authored a course online before. Is there help?")}</h3>
<p>Absolutely. We have created an online course, edX101, that describes some best practices: from filming video, creating exercises, to the basics of running an online course. Additionally, we're always here to help, just drop us a note.</p> <p>${_("Absolutely. We have created an online course, edX101, that describes some best practices: from filming video, creating exercises, to the basics of running an online course. Additionally, we're always here to help, just drop us a note.")}</p>
</div> </div>
</aside> </aside>
</section> </section>
...@@ -104,7 +105,7 @@ ...@@ -104,7 +105,7 @@
}).blur(function() { }).blur(function() {
$("label").removeClass("is-focused"); $("label").removeClass("is-focused");
}); });
function getCookie(name) { function getCookie(name) {
return $.cookie(name); return $.cookie(name);
...@@ -138,4 +139,4 @@ ...@@ -138,4 +139,4 @@
}); });
})(this) })(this)
</script> </script>
</%block> </%block>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Static Pages</%block> <%block name="title">${_("Static Pages")}</%block>
<%block name="bodyclass">static-pages</%block> <%block name="bodyclass">static-pages</%block>
<%block name="content"> <%block name="content">
...@@ -11,24 +12,24 @@ ...@@ -11,24 +12,24 @@
</div> </div>
<article class="static-page-overview"> <article class="static-page-overview">
<a href="#" class="new-static-page-button wip-box"><span class="plus-icon"></span> New Static Page</a> <a href="#" class="new-static-page-button wip-box"><span class="plus-icon"></span> ${_("New Static Page")}</a>
<ul class="static-page-list"> <ul class="static-page-list">
<li class="static-page-item"> <li class="static-page-item">
<a href="#" class="page-name">Course Info</a> <a href="#" class="page-name">${_("Course Info")}</a>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a> <a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a> <a href="#" class="drag-handle wip"></a>
</div> </div>
</li> </li>
<li class="static-page-item"> <li class="static-page-item">
<a href="#" class="page-name">Textbook</a> <a href="#" class="page-name">${_("Textbook")}</a>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a> <a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a> <a href="#" class="drag-handle wip"></a>
</div> </div>
</li> </li>
<li class="static-page-item"> <li class="static-page-item">
<a href="#" class="page-name">Syllabus</a> <a href="#" class="page-name">${_("Syllabus")}</a>
<div class="item-actions"> <div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a> <a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a> <a href="#" class="drag-handle wip"></a>
...@@ -38,4 +39,4 @@ ...@@ -38,4 +39,4 @@
</article> </article>
</div> </div>
</div> </div>
</%block> </%block>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment