Commit 39714a17 by cahrens

Merge branch 'master' into bug/christina/cursor-move

Conflicts:
	cms/static/js/base.js
parents eebef1e6 2dcfd1eb
Feature: Course checklists
Scenario: A course author sees checklists defined by edX
Given I have opened a new course in Studio
When I select Checklists from the Tools menu
Then I see the four default edX checklists
Scenario: A course author can mark tasks as complete
Given I have opened Checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
Then I am brought to the course outline page
And I press the browser back button
Then I am brought back to the course outline in the correct state
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page
Then I am brought to the help page in a new window
from lettuce import world, step
from common import *
from terrain.steps import reload_the_page
############### ACTIONS ####################
@step('I select Checklists from the Tools menu$')
def i_select_checklists(step):
expand_icon_css = 'li.nav-course-tools i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css)
link_css = 'li.nav-course-tools-checklists a'
css_click(link_css)
@step('I have opened Checklists$')
def i_have_opened_checklists(step):
step.given('I have opened a new course in Studio')
step.given('I select Checklists from the Tools menu')
@step('I see the four default edX checklists$')
def i_see_default_checklists(step):
checklists = css_find('.checklist-title')
assert_equal(4, len(checklists))
assert_true(checklists[0].text.endswith('Getting Started With Studio'))
assert_true(checklists[1].text.endswith('Draft a Rough Course Outline'))
assert_true(checklists[2].text.endswith("Explore edX\'s Support Tools"))
assert_true(checklists[3].text.endswith('Draft Your Course About Page'))
@step('I can check and uncheck tasks in a checklist$')
def i_can_check_and_uncheck_tasks(step):
# Use the 2nd checklist as a reference
verifyChecklist2Status(0, 7, 0)
toggleTask(1, 0)
verifyChecklist2Status(1, 7, 14)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
toggleTask(1, 6)
verifyChecklist2Status(3, 7, 43)
toggleTask(1, 3)
verifyChecklist2Status(2, 7, 29)
@step('They are correctly selected after I reload the page$')
def tasks_correctly_selected_after_reload(step):
reload_the_page(step)
verifyChecklist2Status(2, 7, 29)
# verify that task 7 is still selected by toggling its checkbox state and making sure that it deselects
toggleTask(1, 6)
verifyChecklist2Status(1, 7, 14)
@step('I select a link to the course outline$')
def i_select_a_link_to_the_course_outline(step):
clickActionLink(1, 0, 'Edit Course Outline')
@step('I am brought to the course outline page$')
def i_am_brought_to_course_outline(step):
assert_equal('Course Outline', css_find('.outline .title-1')[0].text)
assert_equal(1, len(world.browser.windows))
@step('I am brought back to the course outline in the correct state$')
def i_am_brought_back_to_course_outline(step):
step.given('I see the four default edX checklists')
# In a previous step, we selected (1, 0) in order to click the 'Edit Course Outline' link.
# Make sure the task is still showing as selected (there was a caching bug with the collection).
verifyChecklist2Status(1, 7, 14)
@step('I select a link to help page$')
def i_select_a_link_to_the_help_page(step):
clickActionLink(2, 0, 'Visit Studio Help')
@step('I am brought to the help page in a new window$')
def i_am_brought_to_help_page_in_new_window(step):
step.given('I see the four default edX checklists')
windows = world.browser.windows
assert_equal(2, len(windows))
world.browser.switch_to_window(windows[1])
assert_equal('http://help.edge.edx.org/', world.browser.url)
############### HELPER METHODS ####################
def verifyChecklist2Status(completed, total, percentage):
def verify_count(driver):
try:
statusCount = css_find('#course-checklist1 .status-count').first
return statusCount.text == str(completed)
except StaleElementReferenceException:
return False
wait_for(verify_count)
assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text)
# Would like to check the CSS width, but not sure how to do that.
assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text)
def toggleTask(checklist, task):
css_click('#course-checklist' + str(checklist) +'-task' + str(task))
def clickActionLink(checklist, task, actionText):
# toggle checklist item to make sure that the link button is showing
toggleTask(checklist, task)
action_link = css_find('#course-checklist' + str(checklist) + ' a')[task]
# text will be empty initially, wait for it to populate
def verify_action_link_text(driver):
return action_link.text == actionText
wait_for(verify_action_link_text)
action_link.click()
""" Unit tests for checklist methods in views.py. """
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
import json
class ChecklistTestCase(CourseTestCase):
""" Test for checklist get and put methods. """
def setUp(self):
""" Creates the test course. """
super(ChecklistTestCase, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course')
def get_persisted_checklists(self):
""" Returns the checklists as persisted in the modulestore. """
modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists
def test_get_checklists(self):
""" Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course)
response = self.client.get(checklists_url)
self.assertContains(response, "Getting Started With Studio")
payload = response.content
# Now delete the checklists from the course and verify they get repopulated (for courses
# created before checklists were introduced).
self.course.checklists = None
modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course))
self.assertEquals(self.get_persisted_checklists(), None)
response = self.client.get(checklists_url)
self.assertEquals(payload, response.content)
def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """
update_url = reverse('checklists_updates', kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 1})
returned_checklists = json.loads(self.client.get(update_url).content)
self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name})
response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index_out_of_range(self):
""" Checklist index out of range, will error on post. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index(self):
""" Check that an update of a particular checklist works. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 2})
payload = self.course.checklists[2]
self.assertFalse(payload.get('is_checked'))
payload['is_checked'] = True
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
self.assertTrue(returned_checklist.get('is_checked'))
self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """
update_url = reverse('checklists_updates', kwargs={'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.delete(update_url)
self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
from contentstore import utils
""" Tests for utils. """
from contentstore import utils
import mock
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from .utils import ModuleStoreTestCase
class LMSLinksTestCase(TestCase):
""" Tests for LMS links. """
def about_page_test(self):
""" Get URL for about page. """
location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
def ls_link_test(self):
def lms_link_test(self):
""" Tests get_lms_link_for_item. """
location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
link = utils.get_lms_link_for_item(location, True)
self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
self.assertEquals(
link,
"//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
)
class UrlReverseTestCase(ModuleStoreTestCase):
""" Tests for get_url_reverse """
def test_CoursePageNames(self):
""" Test the defined course pages. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'/manage_users/i4x://mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('ManageUsers', course)
)
self.assertEquals(
'/mitX/666/settings-details/URL_Reverse_Course',
utils.get_url_reverse('SettingsDetails', course)
)
self.assertEquals(
'/mitX/666/settings-grading/URL_Reverse_Course',
utils.get_url_reverse('SettingsGrading', course)
)
self.assertEquals(
'/mitX/666/course/URL_Reverse_Course',
utils.get_url_reverse('CourseOutline', course)
)
self.assertEquals(
'/mitX/666/checklists/URL_Reverse_Course',
utils.get_url_reverse('Checklists', course)
)
def test_unknown_passes_through(self):
""" Test that unknown values pass through. """
course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course')
self.assertEquals(
'foobar',
utils.get_url_reverse('foobar', course)
)
self.assertEquals(
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
\ No newline at end of file
......@@ -2,6 +2,7 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
......@@ -158,3 +159,35 @@ def update_item(location, value):
get_modulestore(location).delete_item(location)
else:
get_modulestore(location).update_item(location, value)
def get_url_reverse(course_page_name, course_module):
"""
Returns the course URL link to the specified location. This value is suitable to use as an href link.
course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers'
or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of
course_page_names so that it can also be used for absolute (known) URLs.
course_module is used to obtain the location, org, course, and name properties for a course, if
course_page_name corresponds to an attribute in CoursePageNames.
"""
url_name = getattr(CoursePageNames, course_page_name, None)
ctx_loc = course_module.location
if CoursePageNames.ManageUsers == url_name:
return reverse(url_name, kwargs={"location": ctx_loc})
elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading,
CoursePageNames.CourseOutline, CoursePageNames.Checklists]:
return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name})
else:
return course_page_name
class CoursePageNames:
""" Constants for pages that are recognized by get_url_reverse method. """
ManageUsers = "manage_users"
SettingsDetails = "settings_details"
SettingsGrading = "settings_grading"
CourseOutline = "course_index"
Checklists = "checklists"
......@@ -10,7 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
@classmethod
def fetch(cls, course_location):
......
<% var allChecked = itemsChecked == items.length; %>
<section
<% if (allChecked) { %>
class="course-checklist is-completed"
<% } else { %>
class="course-checklist"
<% } %>
id="<%= 'course-checklist' + checklistIndex %>">
<% var widthPercentage = 'width:' + percentChecked + '%;'; %>
<span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value" style="<%= widthPercentage %>">
<span class="int"><%= percentChecked %></span>% of checklist completed</span></span>
<header>
<h3 class="checklist-title title-2 is-selectable" title="Collapse/Expand this Checklist">
<i class="ss-icon ss-symbolicons-standard icon-arrow ui-toggle-expansion">&#x25BE;</i>
<%= checklistShortDescription %></h3>
<span class="checklist-status status">
Tasks Completed: <span class="status-count"><%= itemsChecked %></span>/<span class="status-amount"><%= items.length %></span>
<i class="ss-icon ss-symbolicons-standard icon-confirm">&#x2713;</i>
</span>
</header>
<ul class="list list-tasks">
<% var taskIndex = 0; %>
<% _.each(items, function(item) { %>
<% var checked = item['is_checked']; %>
<li
<% if (checked) { %>
class="task is-completed"
<% } else { %>
class="task"
<% } %>
>
<% var taskId = 'course-checklist' + checklistIndex + '-task' + taskIndex; %>
<input type="checkbox" class="task-input" data-checklist="<%= checklistIndex %>" data-task="<%= taskIndex %>"
name="<%= taskId %>" id="<%= taskId %>"
<% if (checked) { %>
checked="checked"
<% } %>
>
<label class="task-details" for="<%= taskId %>">
<h4 class="task-name title title-3"><%= item['short_description'] %></h4>
<p class="task-description"><%= item['long_description'] %></p>
</label>
<% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
<ul class="list-actions task-actions">
<li>
<a href="<%= item['action_url'] %>" class="action action-primary"
<% if (item['action_external']) { %>
rel="external" title="This link will open in a new browser window/tab"
<% } %>
><%= item['action_text'] %></a>
</li>
</ul>
<% } %>
</li>
<% taskIndex+=1; }) %>
</ul>
</section>
\ No newline at end of file
......@@ -14,10 +14,6 @@ $(document).ready(function () {
// scopes (namely the course-info tab)
window.$modalCover = $modalCover;
// Control whether template caching in local memory occurs (see template_loader.js). Caching screws up development but may
// be a good optimization in production (it works fairly well)
window.cachetemplates = false;
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component');
......@@ -76,10 +72,7 @@ $(document).ready(function () {
});
// general link management - new window/tab
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').click(function (e) {
window.open($(this).attr('href'));
e.preventDefault();
});
$('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow);
// general link management - lean modal window
$('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({overlay: 0.50, closeButton: '.action-modal-close' });
......@@ -87,6 +80,10 @@ $(document).ready(function () {
(e).preventDefault();
});
// general link management - smooth scrolling page links
$('a[rel*="view"]').bind('click', linkSmoothScroll);
// toggling overview section details
$(function () {
if ($('.courseware-section').length > 0) {
......@@ -151,6 +148,29 @@ $(document).ready(function () {
});
});
function linkSmoothScroll(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $(this).attr('href')
});
console.log('clicked!');
}
function linkNewWindow(e) {
window.open($(e.target).attr('href'));
e.preventDefault();
}
// On AWS instances, base.js gets wrapped in a separate scope as part of Django static
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window,
// when we can access it from other scopes (namely the checklists)
window.cmsLinkNewWindow = linkNewWindow;
function toggleSections(e) {
e.preventDefault();
......
// Model for checklists_view.js.
CMS.Models.Checklist = Backbone.Model.extend({
});
CMS.Models.ChecklistCollection = Backbone.Collection.extend({
model : CMS.Models.Checklist,
parse: function(response) {
_.each(response,
function( element, idx ) {
element.id = idx;
});
return response;
},
// Disable caching so the browser back button will work (checklists have links to other
// places within Studio).
fetch: function (options) {
options.cache = false;
return Backbone.Collection.prototype.fetch.call(this, options);
}
});
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
// so this only loads the lazily loaded ones.
(function() {
if (typeof window.templateLoader == 'function') return;
(function () {
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.16",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url : filename,
success : function(data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error : function(xhdr, textStatus, errorThrown) {
console.log(textStatus); },
dataType : "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function(templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function() {
try {
// window.cachetemplates is global set in base.js to turn caching on/off
return window.cachetemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function() {
if (this.localStorageAvailable) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function() {
if (this.localStorageAvailable) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
templateVersion: "0.0.15",
templates: {},
// Control whether template caching in local memory occurs. Caching screws up development but may
// be a good optimization in production (it works fairly well).
cacheTemplates: false,
loadRemoteTemplate: function (templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.ajax({url: filename,
success: function (data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
},
error: function (xhdr, textStatus, errorThrown) {
console.log(textStatus);
},
dataType: "html"
})
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function (templateName, data) {
// is there a reason this doesn't go ahead and compile the template? _.template(data)
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
// if it maintains a separate cache of uncompiled ones
this.templates[templateName] = data;
},
localStorageAvailable: function () {
try {
return this.cacheTemplates && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function () {
if (this.localStorageAvailable()) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function () {
if (this.localStorageAvailable()) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
};
templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader;
})();
})();
if (!CMS.Views['Checklists']) CMS.Views.Checklists = {};
CMS.Views.Checklists = Backbone.View.extend({
// takes CMS.Models.Checklists as model
events : {
'click .course-checklist .checklist-title' : "toggleChecklist",
'click .course-checklist .task input' : "toggleTask",
'click a[rel="external"]' : window.cmsLinkNewWindow
},
initialize : function() {
var self = this;
this.collection.fetch({
complete: function () {
window.templateLoader.loadRemoteTemplate("checklist",
"/static/client_templates/checklist.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
},
error: CMS.ServerError
}
);
},
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
this.$el.empty();
var self = this;
_.each(this.collection.models,
function(checklist, index) {
self.$el.append(self.renderTemplate(checklist, index));
});
return this;
},
renderTemplate: function (checklist, index) {
var checklistItems = checklist.attributes['items'];
var itemsChecked = 0;
_.each(checklistItems,
function(checklist) {
if (checklist['is_checked']) {
itemsChecked +=1;
}
});
var percentChecked = Math.round((itemsChecked/checklistItems.length)*100);
return this.template({
checklistIndex : index,
checklistShortDescription : checklist.attributes['short_description'],
items: checklistItems,
itemsChecked: itemsChecked,
percentChecked: percentChecked});
},
toggleChecklist : function(e) {
e.preventDefault();
$(e.target).closest('.course-checklist').toggleClass('is-collapsed');
},
toggleTask : function (e) {
var self = this;
var completed = 'is-completed';
var $checkbox = $(e.target);
var $task = $checkbox.closest('.task');
$task.toggleClass(completed);
var checklist_index = $checkbox.data('checklist');
var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({},
{
success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
},
error : CMS.ServerError
});
}
});
\ No newline at end of file
......@@ -214,7 +214,7 @@ h1 {
color: $gray-l2;
}
.title, .title-1 {
.title-1 {
@include font-size(32);
margin: 0;
padding: 0;
......@@ -283,8 +283,8 @@ h1 {
.title-3 {
@include font-size(16);
margin: 0 0 ($baseline/4) 0;
font-weight: 500;
margin: 0 0 ($baseline/2) 0;
font-weight: 600;
}
.title-4 {
......@@ -327,7 +327,8 @@ h1 {
}
}
.nav-related {
// navigation
.nav-related, .nav-page {
.nav-item {
margin-bottom: ($baseline/4);
......@@ -691,6 +692,10 @@ hr.divide {
word-wrap: break-word;
}
hr.divider {
@extend .sr;
}
// ====================
// js dependant
......
......@@ -34,6 +34,7 @@ $gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
......@@ -161,4 +162,4 @@ $disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
\ No newline at end of file
$error-red: rgb(253, 87, 87);
......@@ -43,6 +43,7 @@
@import 'views/subsection';
@import 'views/unit';
@import 'views/users';
@import 'views/checklists';
@import 'assets/content-types';
......
// Studio - Course Settings
// ====================
body.course.checklists {
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(9, 12);
margin-right: flex-gutter();
}
// checklists - general
.course-checklist {
@extend .window;
margin: 0 0 ($baseline*2) 0;
&:last-child {
margin-bottom: 0;
}
// visual status
.viz-checklist-status {
@include text-hide();
@include size(100%,($baseline/4));
position: relative;
display: block;
margin: 0;
background: $gray-l4;
.viz-checklist-status-value {
@include transition(width 2s ease-in-out .25s);
position: absolute;
top: 0;
left: 0;
width: 0%;
height: ($baseline/4);
background: $green;
.int {
@include text-sr();
}
}
}
// <span class="viz viz-checklist-status"><span class="viz value viz-checklist-status-value"><span class="int">0</span>% of checklist completed</span></span>
// header/title
header {
@include clearfix();
@include box-shadow(inset 0 -1px 1px $shadow-l1);
margin-bottom: 0;
border-bottom: 1px solid $gray-l3;
padding: $baseline ($baseline*1.5);
.checklist-title {
@include transition(color .15s .25s ease-in-out);
width: flex-grid(6, 9);
margin: 0 flex-gutter() 0 0;
float: left;
.ui-toggle-expansion {
@include transition(rotate .15s ease-in-out .25s);
@include font-size(14);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
color: $gray-l4;
}
&.is-selectable {
cursor: pointer;
&:hover {
color: $blue;
.ui-toggle-expansion {
color: $blue;
}
}
}
}
.checklist-status {
@include font-size(13);
width: flex-grid(3, 9);
float: right;
margin-top: ($baseline/2);
text-align: right;
color: $gray-l2;
.icon-confirm {
@include font-size(20);
display: inline-block;
vertical-align: middle;
margin-left: ($baseline/2);
color: $gray-l4;
}
.status-count {
@include font-size(16);
margin-left: ($baseline/4);
margin-right: ($baseline/4);
color: $gray-d3;
font-weight: 600;
}
.status-amount {
@include font-size(16);
margin-left: ($baseline/4);
color: $gray-d3;
font-weight: 600;
}
}
}
// checklist actions
.course-checklist-actions {
@include clearfix();
@include box-shadow(inset 0 1px 1px $shadow-l1);
@include transition(border .15s ease-in-out .25s);
border-top: 1px solid $gray-l2;
padding: $baseline ($baseline*1.5);
background: $gray-l4;
.action-primary {
@include green-button();
float: left;
.icon-add {
@include font-size(12);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
.action-secondary {
@include font-size(14);
@include grey-button();
font-weight: 400;
float: right;
.icon-delete {
@include font-size(12);
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
}
}
}
// state - collapsed
&.is-collapsed {
header {
@include box-shadow(none);
.checklist-title {
.ui-toggle-expansion {
@include transform(rotate(-90deg));
@include transform-origin(50% 50%);
}
}
}
.list-tasks {
height: 0;
}
}
// state - completed
&.is-completed {
.viz-checklist-status {
.viz-checklist-status-value {
width: 100%;
}
}
header {
.checklist-title, .icon-confirm {
color: $green;
}
.checklist-status {
.status-count, .status-amount, .icon-confirm {
color: $green;
}
}
}
}
// state - not available
.is-not-available {
}
}
// list of tasks
.list-tasks {
height: auto;
overflow: hidden;
.task {
@include transition(background .15s ease-in-out .25s);
@include transition(border .15s ease-in-out .25s);
@include clearfix();
position: relative;
border-top: 1px solid $white;
border-bottom: 1px solid $gray-l5;
padding: $baseline ($baseline*1.5);
background: $white;
opacity: 1.0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
}
.task-input {
display: inline-block;
vertical-align: text-top;
float: left;
margin: ($baseline/2) flex-gutter() 0 0;
}
.task-details {
display: inline-block;
vertical-align: text-top;
float: left;
width: flex-grid(6,9);
font-weight: 500;
.task-name {
@include transition(color .15s .25s ease-in-out);
vertical-align: baseline;
cursor: pointer;
margin-bottom: 0;
}
.task-description {
@include transition(color .15s .25s ease-in-out);
@include font-size(14);
color: $gray-l2;
}
.task-support {
@include transition(opacity .15s .25s ease-in-out);
@include font-size(12);
opacity: 0;
pointer-events: none;
}
}
.task-actions {
@include transition(opacity .15s .25s ease-in-out);
@include clearfix();
display: inline-block;
vertical-align: middle;
float: right;
width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0;
pointer-events: none;
text-align: right;
.action-primary {
@include blue-button;
@include transition(all .15s);
@include font-size(12);
font-weight: 600;
text-align: center;
}
.action-secondary {
@include font-size(13);
margin-top: ($baseline/2);
}
}
// state - hover
&:hover {
background: $blue-l5;
border-bottom-color: $blue-l4;
border-top-color: $blue-l4;
opacity: 1.0;
.task-details {
.task-support {
opacity: 1.0;
pointer-events: auto;
}
}
.task-actions {
opacity: 1.0;
pointer-events: auto;
}
}
// state - completed
&.is-completed {
background: $gray-l6;
border-top-color: $gray-l5;
border-bottom-color: $gray-l5;
.task-name {
color: $gray-l2;
}
.task-actions {
.action-primary {
@include grey-button;
@include font-size(12);
font-weight: 600;
text-align: center;
}
}
&:hover {
background: $gray-l5;
border-bottom-color: $gray-l4;
border-top-color: $gray-l4;
.task-details {
opacity:1.0;
}
}
}
}
}
.content-supplementary {
width: flex-grid(3, 12);
}
}
\ No newline at end of file
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Uploads &amp; Files</%block>
<%block name="title">Files &amp; Uploads</%block>
<%namespace name='static' file='static_content.html'/>
......
......@@ -45,6 +45,7 @@
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
<script src="${static.url('js/vendor/jquery.form.js')}"></script>
<script src="${static.url('js/vendor/jquery.smooth-scroll.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript">
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Checklists</%block>
<%block name="bodyclass">is-signedin course uxdesign checklists</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript">
$(document).ready(function () {
var checklistCollection = new CMS.Models.ChecklistCollection();
checklistCollection.url = "${reverse('checklists_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Checklists({
el: $('.course-checklists'),
collection: checklistCollection
});
});
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Checklists</h1>
</div>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<form id="course-checklists" class="course-checklists" method="post" action="">
<h2 class="title title-3 sr">Current Checklists</h2>
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title title-3">What are checklists?</h3>
<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.
</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.
</p>
</div>
<div class="bit">
<h3 class="title title-3">Studio checklists</h3>
<nav class="nav-page checklists-current">
<ol>
% for checklist in checklists:
<li class="nav-item">
<a rel="view" href="${'#course-checklist' + str(loop.index)}">${checklist['short_description']}</a>
</li>
% endfor
</ol>
</nav>
</div>
</aside>
</section>
</div>
</%block>
......@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title -->
<%block name="title">Updates</%block>
<%block name="title">Course Updates</%block>
<%block name="bodyclass">is-signedin course course-info updates</%block>
......
......@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Export Course</%block>
<%block name="title">Course Export</%block>
<%block name="bodyclass">is-signedin course tools export</%block>
<%block name="content">
......
......@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Import Course</%block>
<%block name="title">Course Import</%block>
<%block name="bodyclass">is-signedin course tools import</%block>
<%block name="content">
......
<%inherit file="base.html" />
<%block name="title">Courses</%block>
<%block name="title">My Courses</%block>
<%block name="bodyclass">is-signedin index dashboard</%block>
<%block name="header_extras">
......
<%inherit file="base.html" />
<%block name="title">Course Staff Manager</%block>
<%block name="title">Course Team Settings</%block>
<%block name="bodyclass">is-signedin course users settings team</%block>
......
<%inherit file="base.html" />
<%block name="title">Schedule &amp; Details</%block>
<%block name="title">Schedule &amp; Details Settings</%block>
<%block name="bodyclass">is-signedin course schedule settings</%block>
<%namespace name='static' file='static_content.html'/>
......
<%inherit file="base.html" />
<%block name="title">Grading</%block>
<%block name="title">Grading Settings</%block>
<%block name="bodyclass">is-signedin course grading settings</%block>
<%namespace name='static' file='static_content.html'/>
......
......@@ -40,7 +40,7 @@
This unit was originally published on ${published_date}.
% endif
</p>
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">Preview the published version</a>
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">View the Live Version</a>
</div>
<div class="main-column">
<article class="unit-body window">
......
......@@ -56,6 +56,7 @@
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
<li class="nav-item nav-course-tools-checklists"><a href="${reverse('checklists', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Checklists</a></li>
<li class="nav-item nav-course-tools-import"><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Import</a></li>
<li class="nav-item nav-course-tools-export"><a href="${reverse('export_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Export</a></li>
</ul>
......
......@@ -44,8 +44,8 @@ urlpatterns = ('',
'contentstore.views.remove_user', name='remove_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$', 'contentstore.views.course_info', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info_json'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='settings_details'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='settings_grading'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
# This is the URL to initially render the course advanced settings.
......@@ -83,6 +83,9 @@ urlpatterns = ('',
# User creation and updating views
urlpatterns += (
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$',
'contentstore.views.update_checklist', name='checklists_updates'),
url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'),
url(r'^signup$', 'contentstore.views.signup', name='signup'),
......
......@@ -30,6 +30,11 @@ def reload_the_page(step):
world.browser.reload()
@step('I press the browser back button$')
def browser_back(step):
world.browser.driver.back()
@step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step):
world.browser.visit(django_url('/'))
......
......@@ -179,7 +179,7 @@ class CourseFields(object):
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
has_children = True
checklists=List(scope=Scope.settings)
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
# An extra property is used rather than the wiki_slug/number because
......
......@@ -2,5 +2,123 @@
metadata:
display_name: Empty
start: 2020-10-10T10:00
checklists: [
{"short_description" : "Getting Started With Studio",
"items" : [{"short_description": "Add Course Team Members",
"long_description": "Grant your collaborators permission to edit your course so you can work together.",
"is_checked": false,
"action_url": "ManageUsers",
"action_text": "Edit Course Team",
"action_external": false},
{"short_description": "Set Important Dates for Your Course",
"long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
"is_checked": false,
"action_url": "SettingsDetails",
"action_text": "Edit Course Details &amp; Schedule",
"action_external": false},
{"short_description": "Draft Your Course's Grading Policy",
"long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.",
"is_checked": false,
"action_url": "SettingsGrading",
"action_text": "Edit Grading Settings",
"action_external": false},
{"short_description": "Explore the Other Studio Checklists",
"long_description": "Discover other available course authoring tools, and find help when you need it.",
"is_checked": false,
"action_url": "",
"action_text": "",
"action_external": false}]
},
{"short_description" : "Draft a Rough Course Outline",
"items" : [{"short_description": "Create Your First Section and Subsection",
"long_description": "Use your course outline to build your first Section and Subsection.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Set Section Release Dates",
"long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Designate a Subsection as Graded",
"long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Reordering Course Content",
"long_description": "Use drag and drop to reorder the content in your course.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Renaming Sections",
"long_description": "Rename Sections by clicking the Section name from the Course Outline.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Deleting Course Content",
"long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false},
{"short_description": "Add an Instructor-Only Section to Your Outline",
"long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
"is_checked": false,
"action_url": "CourseOutline",
"action_text": "Edit Course Outline",
"action_external": false}]
},
{"short_description" : "Explore edX's Support Tools",
"items" : [{"short_description": "Explore the Studio Help Forum",
"long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
"is_checked": false,
"action_url": "http://help.edge.edx.org/",
"action_text": "Visit Studio Help",
"action_external": true},
{"short_description": "Enroll in edX 101",
"long_description": "Register for edX 101, edX's primer for course creation.",
"is_checked": false,
"action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
"action_text": "Register for edX 101",
"action_external": true},
{"short_description": "Download the Studio Documentation",
"long_description": "Download the searchable Studio reference documentation in PDF form.",
"is_checked": false,
"action_url": "http://help.edge.edx.org/help/assets/8ccd409f979c8639dd463e126eb840dc67f09098/Getting_Started_with_Studio.pdf",
"action_text": "Download Documentation",
"action_external": true}]
},
{"short_description" : "Draft Your Course About Page",
"items" : [{"short_description": "Draft a Course Description",
"long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.",
"is_checked": false,
"action_url": "SettingsDetails",
"action_text": "Edit Course Schedule &amp; Details",
"action_external": false},
{"short_description": "Add Staff Bios",
"long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
"is_checked": false,
"action_url": "SettingsDetails",
"action_text": "Edit Course Schedule &amp; Details",
"action_external": false},
{"short_description": "Add Course FAQs",
"long_description": "Include a short list of frequently asked questions about your course.",
"is_checked": false,
"action_url": "SettingsDetails",
"action_text": "Edit Course Schedule &amp; Details",
"action_external": false},
{"short_description": "Add Course Prerequisites",
"long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.",
"is_checked": false,
"action_url": "SettingsDetails",
"action_text": "Edit Course Schedule &amp; Details",
"action_external": false}]
}
]
data: { 'textbooks' : [ ], 'wiki_slug' : null }
children: []
/*!
* Smooth Scroll - v1.4.10 - 2013-03-02
* https://github.com/kswedberg/jquery-smooth-scroll
* Copyright (c) 2013 Karl Swedberg
* Licensed MIT (https://github.com/kswedberg/jquery-smooth-scroll/blob/master/LICENSE-MIT)
*/
(function(l){function t(l){return l.replace(/(:|\.)/g,"\\$1")}var e="1.4.10",o={exclude:[],excludeWithin:[],offset:0,direction:"top",scrollElement:null,scrollTarget:null,beforeScroll:function(){},afterScroll:function(){},easing:"swing",speed:400,autoCoefficent:2},r=function(t){var e=[],o=!1,r=t.dir&&"left"==t.dir?"scrollLeft":"scrollTop";return this.each(function(){if(this!=document&&this!=window){var t=l(this);t[r]()>0?e.push(this):(t[r](1),o=t[r]()>0,o&&e.push(this),t[r](0))}}),e.length||this.each(function(){"BODY"===this.nodeName&&(e=[this])}),"first"===t.el&&e.length>1&&(e=[e[0]]),e};l.fn.extend({scrollable:function(l){var t=r.call(this,{dir:l});return this.pushStack(t)},firstScrollable:function(l){var t=r.call(this,{el:"first",dir:l});return this.pushStack(t)},smoothScroll:function(e){e=e||{};var o=l.extend({},l.fn.smoothScroll.defaults,e),r=l.smoothScroll.filterPath(location.pathname);return this.unbind("click.smoothscroll").bind("click.smoothscroll",function(e){var n=this,s=l(this),c=o.exclude,i=o.excludeWithin,a=0,f=0,h=!0,u={},d=location.hostname===n.hostname||!n.hostname,m=o.scrollTarget||(l.smoothScroll.filterPath(n.pathname)||r)===r,p=t(n.hash);if(o.scrollTarget||d&&m&&p){for(;h&&c.length>a;)s.is(t(c[a++]))&&(h=!1);for(;h&&i.length>f;)s.closest(i[f++]).length&&(h=!1)}else h=!1;h&&(e.preventDefault(),l.extend(u,o,{scrollTarget:o.scrollTarget||p,link:n}),l.smoothScroll(u))}),this}}),l.smoothScroll=function(t,e){var o,r,n,s,c=0,i="offset",a="scrollTop",f={},h={};"number"==typeof t?(o=l.fn.smoothScroll.defaults,n=t):(o=l.extend({},l.fn.smoothScroll.defaults,t||{}),o.scrollElement&&(i="position","static"==o.scrollElement.css("position")&&o.scrollElement.css("position","relative"))),o=l.extend({link:null},o),a="left"==o.direction?"scrollLeft":a,o.scrollElement?(r=o.scrollElement,c=r[a]()):r=l("html, body").firstScrollable(),o.beforeScroll.call(r,o),n="number"==typeof t?t:e||l(o.scrollTarget)[i]()&&l(o.scrollTarget)[i]()[o.direction]||0,f[a]=n+c+o.offset,s=o.speed,"auto"===s&&(s=f[a]||r.scrollTop(),s/=o.autoCoefficent),h={duration:s,easing:o.easing,complete:function(){o.afterScroll.call(o.link,o)}},o.step&&(h.step=o.step),r.length?r.stop().animate(f,h):o.afterScroll.call(o.link,o)},l.smoothScroll.version=e,l.smoothScroll.filterPath=function(l){return l.replace(/^\//,"").replace(/(index|default).[a-zA-Z]{3,4}$/,"").replace(/\/$/,"")},l.fn.smoothScroll.defaults=o})(jQuery);
\ No newline at end of file
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