Commit 596e1ead by Felix Sun

Merge branch 'felix/hinter2' of https://github.com/edx/edx-platform into felix/hinter2

Conflicts:
	common/lib/xmodule/xmodule/crowdsource_hinter.py
parents 3418feba dd8f1f53
...@@ -78,3 +78,4 @@ Peter Fogg <peter.p.fogg@gmail.com> ...@@ -78,3 +78,4 @@ Peter Fogg <peter.p.fogg@gmail.com>
Bethany LaPenta <lapentab@mit.edu> Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com> Renzo Lucioni <renzolucioni@gmail.com>
Felix Sun <felixsun@mit.edu> Felix Sun <felixsun@mit.edu>
Adam Palay <adam@edx.org>
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ 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: Student information is now passed to the tracking log via POST instead of GET.
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
...@@ -13,6 +15,8 @@ LMS: Users are no longer auto-activated if they click "reset password" ...@@ -13,6 +15,8 @@ LMS: Users are no longer auto-activated if they click "reset password"
This is now done when they click on the link in the reset password This is now done when they click on the link in the reset password
email they receive (along with usual path through activation email). email they receive (along with usual path through activation email).
LMS: Fixed a reflected XSS problem in the static textbook views.
LMS: Problem rescoring. Added options on the Grades tab of the LMS: Problem rescoring. Added options on the Grades tab of the
Instructor Dashboard to allow a particular student's submission for a Instructor Dashboard to allow a particular student's submission for a
particular problem to be rescored. Provides an option to see a particular problem to be rescored. Provides an option to see a
......
...@@ -152,6 +152,12 @@ otherwise noted. ...@@ -152,6 +152,12 @@ otherwise noted.
Please see ``LICENSE.txt`` for details. Please see ``LICENSE.txt`` for details.
Documentation
------------
High-level documentation of the code is located in the `doc` subdirectory. Start
with `overview.md` to get an introduction to the architecture of the system.
How to Contribute How to Contribute
----------------- -----------------
......
...@@ -115,7 +115,7 @@ def clickActionLink(checklist, task, actionText): ...@@ -115,7 +115,7 @@ def clickActionLink(checklist, task, actionText):
# text will be empty initially, wait for it to populate # text will be empty initially, wait for it to populate
def verify_action_link_text(driver): def verify_action_link_text(driver):
return action_link.text == actionText return world.css_text('#course-checklist' + str(checklist) + ' a', index=task) == actionText
world.wait_for(verify_action_link_text) world.wait_for(verify_action_link_text)
action_link.click() world.css_click('#course-checklist' + str(checklist) + ' a', index=task)
Feature: Component Adding
As a course author, I want to be able to add a wide variety of components
@skip
Scenario: I can add components
Given I have opened a new course in studio
And I am editing a new unit
When I add the following components:
| Component |
| Discussion |
| Blank HTML |
| LaTex |
| Blank Problem|
| Dropdown |
| Multi Choice |
| Numerical |
| Text Input |
| Advanced |
| Circuit |
| Custom Python|
| Image Mapped |
| Math Input |
| Problem LaTex|
| Adaptive Hint|
| Video |
Then I see the following components:
| Component |
| Discussion |
| Blank HTML |
| LaTex |
| Blank Problem|
| Dropdown |
| Multi Choice |
| Numerical |
| Text Input |
| Advanced |
| Circuit |
| Custom Python|
| Image Mapped |
| Math Input |
| Problem LaTex|
| Adaptive Hint|
| Video |
@skip
Scenario: I can delete Components
Given I have opened a new course in studio
And I am editing a new unit
And I add the following components:
| Component |
| Discussion |
| Blank HTML |
| LaTex |
| Blank Problem|
| Dropdown |
| Multi Choice |
| Numerical |
| Text Input |
| Advanced |
| Circuit |
| Custom Python|
| Image Mapped |
| Math Input |
| Problem LaTex|
| Adaptive Hint|
| Video |
When I will confirm all alerts
And I delete all components
Then I see no components
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true
DATA_LOCATION = 'i4x://edx/templates'
@step(u'I am editing a new unit')
def add_unit(step):
css_selectors = ['a.new-courseware-section-button', 'input.new-section-name-save', 'a.new-subsection-item',
'input.new-subsection-name-save', 'div.section-item a.expand-collapse-icon', 'a.new-unit-item']
for selector in css_selectors:
world.css_click(selector)
@step(u'I add the following components:')
def add_components(step):
for component in [step_hash['Component'] for step_hash in step.hashes]:
assert component in COMPONENT_DICTIONARY
for css in COMPONENT_DICTIONARY[component]['steps']:
world.css_click(css)
@step(u'I see the following components')
def check_components(step):
for component in [step_hash['Component'] for step_hash in step.hashes]:
assert component in COMPONENT_DICTIONARY
assert_true(COMPONENT_DICTIONARY[component]['found_func'](), "{} couldn't be found".format(component))
@step(u'I delete all components')
def delete_all_components(step):
for _ in range(len(COMPONENT_DICTIONARY)):
world.css_click('a.delete-button')
@step(u'I see no components')
def see_no_components(steps):
assert world.is_css_not_present('li.component')
def step_selector_list(data_type, path, index=1):
selector_list = ['a[data-type="{}"]'.format(data_type)]
if index != 1:
selector_list.append('a[id="ui-id-{}"]'.format(index))
if path is not None:
selector_list.append('a[data-location="{}/{}/{}"]'.format(DATA_LOCATION, data_type, path))
return selector_list
def found_text_func(text):
return lambda: world.browser.is_text_present(text)
def found_css_func(css):
return lambda: world.is_css_present(css, wait_time=2)
COMPONENT_DICTIONARY = {
'Discussion': {
'steps': step_selector_list('discussion', None),
'found_func': found_css_func('section.xmodule_DiscussionModule')
},
'Blank HTML': {
'steps': step_selector_list('html', 'Blank_HTML_Page'),
#this one is a blank html so a more refined search is being done
'found_func': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')]
},
'LaTex': {
'steps': step_selector_list('html', 'E-text_Written_in_LaTeX'),
'found_func': found_text_func('EXAMPLE: E-TEXT PAGE')
},
'Blank Problem': {
'steps': step_selector_list('problem', 'Blank_Common_Problem'),
'found_func': found_text_func('BLANK COMMON PROBLEM')
},
'Dropdown': {
'steps': step_selector_list('problem', 'Dropdown'),
'found_func': found_text_func('DROPDOWN')
},
'Multi Choice': {
'steps': step_selector_list('problem', 'Multiple_Choice'),
'found_func': found_text_func('MULTIPLE CHOICE')
},
'Numerical': {
'steps': step_selector_list('problem', 'Numerical_Input'),
'found_func': found_text_func('NUMERICAL INPUT')
},
'Text Input': {
'steps': step_selector_list('problem', 'Text_Input'),
'found_func': found_text_func('TEXT INPUT')
},
'Advanced': {
'steps': step_selector_list('problem', 'Blank_Advanced_Problem', index=2),
'found_func': found_text_func('BLANK ADVANCED PROBLEM')
},
'Circuit': {
'steps': step_selector_list('problem', 'Circuit_Schematic_Builder', index=2),
'found_func': found_text_func('CIRCUIT SCHEMATIC BUILDER')
},
'Custom Python': {
'steps': step_selector_list('problem', 'Custom_Python-Evaluated_Input', index=2),
'found_func': found_text_func('CUSTOM PYTHON-EVALUATED INPUT')
},
'Image Mapped': {
'steps': step_selector_list('problem', 'Image_Mapped_Input', index=2),
'found_func': found_text_func('IMAGE MAPPED INPUT')
},
'Math Input': {
'steps': step_selector_list('problem', 'Math_Expression_Input', index=2),
'found_func': found_text_func('MATH EXPRESSION INPUT')
},
'Problem LaTex': {
'steps': step_selector_list('problem', 'Problem_Written_in_LaTeX', index=2),
'found_func': found_text_func('PROBLEM WRITTEN IN LATEX')
},
'Adaptive Hint': {
'steps': step_selector_list('problem', 'Problem_with_Adaptive_Hint', index=2),
'found_func': found_text_func('PROBLEM WITH ADAPTIVE HINT')
},
'Video': {
'steps': step_selector_list('video', None),
'found_func': found_css_func('section.xmodule_VideoModule')
}
}
...@@ -60,8 +60,7 @@ def change_date(_step, new_date): ...@@ -60,8 +60,7 @@ def change_date(_step, new_date):
@step(u'I should see the date "([^"]*)"$') @step(u'I should see the date "([^"]*)"$')
def check_date(_step, date): def check_date(_step, date):
date_css = 'span.date-display' date_css = 'span.date-display'
date_html = world.css_find(date_css) assert date == world.css_html(date_css)
assert date == date_html.html
@step(u'I modify the handout to "([^"]*)"$') @step(u'I modify the handout to "([^"]*)"$')
...@@ -74,8 +73,7 @@ def edit_handouts(_step, text): ...@@ -74,8 +73,7 @@ def edit_handouts(_step, text):
@step(u'I see the handout "([^"]*)"$') @step(u'I see the handout "([^"]*)"$')
def check_handout(_step, handout): def check_handout(_step, handout):
handout_css = 'div.handouts-content' handout_css = 'div.handouts-content'
handouts = world.css_find(handout_css) assert handout in world.css_html(handout_css)
assert handout in handouts.html
def change_text(text): def change_text(text):
......
...@@ -47,7 +47,7 @@ def confirm_change(step): ...@@ -47,7 +47,7 @@ def confirm_change(step):
range_css = '.range' range_css = '.range'
all_ranges = world.css_find(range_css) all_ranges = world.css_find(range_css)
for i in range(len(all_ranges)): for i in range(len(all_ranges)):
assert all_ranges[i].html != '0-50' assert world.css_html(range_css, index=i) != '0-50'
@step(u'I change assignment type "([^"]*)" to "([^"]*)"$') @step(u'I change assignment type "([^"]*)" to "([^"]*)"$')
......
...@@ -9,14 +9,14 @@ from selenium.webdriver.common.keys import Keys ...@@ -9,14 +9,14 @@ from selenium.webdriver.common.keys import Keys
def go_to_static(_step): def go_to_static(_step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages' static_css = 'li.nav-course-courseware-pages'
world.css_find(menu_css).click() world.css_click(menu_css)
world.css_find(static_css).click() world.css_click(static_css)
@step(u'I add a new page') @step(u'I add a new page')
def add_page(_step): def add_page(_step):
button_css = 'a.new-button' button_css = 'a.new-button'
world.css_find(button_css).click() world.css_click(button_css)
@step(u'I should( not)? see a "([^"]*)" static page$') @step(u'I should( not)? see a "([^"]*)" static page$')
...@@ -33,13 +33,13 @@ def click_edit_delete(_step, edit_delete, page): ...@@ -33,13 +33,13 @@ def click_edit_delete(_step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete button_css = 'a.%s-button' % edit_delete
index = get_index(page) index = get_index(page)
assert index != -1 assert index != -1
world.css_find(button_css)[index].click() world.css_click(button_css, index=index)
@step(u'I change the name to "([^"]*)"$') @step(u'I change the name to "([^"]*)"$')
def change_name(_step, new_name): def change_name(_step, new_name):
settings_css = '#settings-mode' settings_css = '#settings-mode'
world.css_find(settings_css).click() world.css_click(settings_css)
input_css = 'input.setting-input' input_css = 'input.setting-input'
name_input = world.css_find(input_css) name_input = world.css_find(input_css)
old_name = name_input.value old_name = name_input.value
...@@ -47,13 +47,13 @@ def change_name(_step, new_name): ...@@ -47,13 +47,13 @@ def change_name(_step, new_name):
name_input._element.send_keys(Keys.END, Keys.BACK_SPACE) name_input._element.send_keys(Keys.END, Keys.BACK_SPACE)
name_input._element.send_keys(new_name) name_input._element.send_keys(new_name)
save_button = 'a.save-button' save_button = 'a.save-button'
world.css_find(save_button).click() world.css_click(save_button)
def get_index(name): def get_index(name):
page_name_css = 'section[data-type="HTMLModule"]' page_name_css = 'section[data-type="HTMLModule"]'
all_pages = world.css_find(page_name_css) all_pages = world.css_find(page_name_css)
for i in range(len(all_pages)): for i in range(len(all_pages)):
if all_pages[i].html == '\n {name}\n'.format(name=name): if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name):
return i return i
return -1 return -1
...@@ -16,14 +16,14 @@ HTTP_PREFIX = "http://localhost:8001" ...@@ -16,14 +16,14 @@ HTTP_PREFIX = "http://localhost:8001"
def go_to_uploads(_step): def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
uploads_css = 'li.nav-course-courseware-uploads' uploads_css = 'li.nav-course-courseware-uploads'
world.css_find(menu_css).click() world.css_click(menu_css)
world.css_find(uploads_css).click() world.css_click(uploads_css)
@step(u'I upload the file "([^"]*)"$') @step(u'I upload the file "([^"]*)"$')
def upload_file(_step, file_name): def upload_file(_step, file_name):
upload_css = 'a.upload-button' upload_css = 'a.upload-button'
world.css_find(upload_css).click() world.css_click(upload_css)
file_css = 'input.file-input' file_css = 'input.file-input'
upload = world.css_find(file_css) upload = world.css_find(file_css)
...@@ -32,7 +32,7 @@ def upload_file(_step, file_name): ...@@ -32,7 +32,7 @@ def upload_file(_step, file_name):
upload._element.send_keys(os.path.abspath(path)) upload._element.send_keys(os.path.abspath(path))
close_css = 'a.close-button' close_css = 'a.close-button'
world.css_find(close_css).click() world.css_click(close_css)
@step(u'I should( not)? see the file "([^"]*)" was uploaded$') @step(u'I should( not)? see the file "([^"]*)" was uploaded$')
...@@ -67,7 +67,7 @@ def no_duplicate(_step, file_name): ...@@ -67,7 +67,7 @@ def no_duplicate(_step, file_name):
all_names = world.css_find(names_css) all_names = world.css_find(names_css)
only_one = False only_one = False
for i in range(len(all_names)): for i in range(len(all_names)):
if file_name == all_names[i].html: if file_name == world.css_html(names_css, index=i):
only_one = not only_one only_one = not only_one
assert only_one assert only_one
...@@ -100,7 +100,7 @@ def get_index(file_name): ...@@ -100,7 +100,7 @@ def get_index(file_name):
names_css = 'td.name-col > a.filename' names_css = 'td.name-col > a.filename'
all_names = world.css_find(names_css) all_names = world.css_find(names_css)
for i in range(len(all_names)): for i in range(len(all_names)):
if file_name == all_names[i].html: if file_name == world.css_html(names_css, index=i):
return i return i
return -1 return -1
......
...@@ -344,6 +344,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -344,6 +344,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
err_cnt = perform_xlint('common/test/data', ['full']) err_cnt = perform_xlint('common/test/data', ['full'])
self.assertGreater(err_cnt, 0) self.assertGreater(err_cnt, 0)
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
def test_module_preview_in_whitelist(self):
'''
Tests the ajax callback to render an XModule
'''
direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full'])
html_module_location = Location(['i4x', 'edX', 'full', 'html', 'html_90', None])
url = reverse('preview_component', kwargs={'location': html_module_location.url()})
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn('Inline content', resp.content)
# also try a custom response which will trigger the 'is this course in whitelist' logic
problem_module_location = Location(['i4x', 'edX', 'full', 'problem', 'H1P1_Energy', None])
url = reverse('preview_component', kwargs={'location': problem_module_location.url()})
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
def test_delete(self): def test_delete(self):
direct_store = modulestore('direct') direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full']) import_from_xml(direct_store, 'common/test/data/', ['full'])
......
"""Tests for CMS's requests to logs"""
from django.test import TestCase
from django.core.urlresolvers import reverse
from contentstore.views.requests import event as cms_user_track
class CMSLogTest(TestCase):
"""
Tests that request to logs from CMS return 204s
"""
def test_post_answers_to_log(self):
"""
Checks that student answer requests submitted to cms's "/event" url
via POST are correctly returned as 204s
"""
requests = [
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
for request_params in requests:
response = self.client.post(reverse(cms_user_track), request_params)
self.assertEqual(response.status_code, 204)
def test_get_answers_to_log(self):
"""
Checks that student answer requests submitted to cms's "/event" url
via GET are correctly returned as 204s
"""
requests = [
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
for request_params in requests:
response = self.client.get(reverse(cms_user_track), request_params)
self.assertEqual(response.status_code, 204)
...@@ -17,10 +17,13 @@ from xmodule.modulestore.mongo import MongoUsage ...@@ -17,10 +17,13 @@ from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel from xblock.runtime import DbModel
from util.sandboxing import can_execute_unsafe_code
import static_replace import static_replace
from .session_kv_store import SessionKeyValueStore from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms from .requests import render_from_lms
from .access import has_access from .access import has_access
from ..utils import get_course_for_item
__all__ = ['preview_dispatch', 'preview_component'] __all__ = ['preview_dispatch', 'preview_component']
...@@ -93,6 +96,8 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -93,6 +96,8 @@ def preview_module_system(request, preview_id, descriptor):
MongoUsage(preview_id, descriptor.location.url()), MongoUsage(preview_id, descriptor.location.url()),
) )
course_id = get_course_for_item(descriptor.location).location.course_id
return ModuleSystem( return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems? # TODO (cpennington): Do we want to track how instructors are using the preview problems?
...@@ -104,6 +109,7 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -104,6 +109,7 @@ def preview_module_system(request, preview_id, descriptor):
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location), replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user, user=request.user,
xblock_model_data=preview_model_data, xblock_model_data=preview_model_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
) )
......
...@@ -105,6 +105,8 @@ ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ...@@ -105,6 +105,8 @@ ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS) MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -140,3 +140,6 @@ SEGMENT_IO_KEY = '***REMOVED***' ...@@ -140,3 +140,6 @@ SEGMENT_IO_KEY = '***REMOVED***'
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# Enabling SQL tracking logs for testing on common/djangoapps/track
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
cms/static/img/logo-edx-studio.png

2.4 KB | W: | H:

cms/static/img/logo-edx-studio.png

4.67 KB | W: | H:

cms/static/img/logo-edx-studio.png
cms/static/img/logo-edx-studio.png
cms/static/img/logo-edx-studio.png
cms/static/img/logo-edx-studio.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -56,11 +56,11 @@ $(document).ready(function() { ...@@ -56,11 +56,11 @@ $(document).ready(function() {
// nav - dropdown related // nav - dropdown related
$body.click(function(e) { $body.click(function(e) {
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dropdown .nav-item .title').removeClass('is-selected'); $('.nav-dd .nav-item .title').removeClass('is-selected');
}); });
$('.nav-dropdown .nav-item .title').click(function(e) { $('.nav-dd .nav-item .title').click(function(e) {
$subnav = $(this).parent().find('.wrapper-nav-sub'); $subnav = $(this).parent().find('.wrapper-nav-sub');
$title = $(this).parent().find('.title'); $title = $(this).parent().find('.title');
...@@ -71,8 +71,8 @@ $(document).ready(function() { ...@@ -71,8 +71,8 @@ $(document).ready(function() {
$subnav.removeClass('is-shown'); $subnav.removeClass('is-shown');
$title.removeClass('is-selected'); $title.removeClass('is-selected');
} else { } else {
$('.nav-dropdown .nav-item .title').removeClass('is-selected'); $('.nav-dd .nav-item .title').removeClass('is-selected');
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected'); $title.addClass('is-selected');
$subnav.addClass('is-shown'); $subnav.addClass('is-shown');
} }
......
...@@ -47,7 +47,7 @@ $gray-d2: shade($gray,40%); ...@@ -47,7 +47,7 @@ $gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%); $gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%); $gray-d4: shade($gray,80%);
$blue: rgb(85, 151, 221); $blue: rgb(0, 159, 230);
$blue-l1: tint($blue,20%); $blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%); $blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%); $blue-l3: tint($blue,60%);
......
...@@ -135,7 +135,48 @@ ...@@ -135,7 +135,48 @@
// ==================== // ====================
// layout-based buttons // simple dropdown button styling - should we move this elsewhere?
.btn-dd {
@extend .btn;
@extend .btn-pill;
padding:($baseline/4) ($baseline/2);
border-width: 1px;
border-style: solid;
border-color: transparent;
text-align: center;
&:hover, &:active {
@extend .fake-link;
border-color: $gray-l3;
}
&.current, &.active, &.is-selected {
@include box-shadow(inset 0 1px 2px 1px $shadow-l1);
border-color: $gray-l3;
}
}
// layout-based buttons - nav dd
.btn-dd-nav-primary {
@extend .btn-dd;
background: $white;
border-color: $white;
color: $gray-d1;
&:hover, &:active {
background: $white;
color: $blue-s1;
}
&.current, &.active {
background: $white;
color: $gray-d4;
&:hover, &:active {
color: $blue-s1;
}
}
}
// ==================== // ====================
......
...@@ -18,20 +18,161 @@ nav { ...@@ -18,20 +18,161 @@ nav {
// ==================== // ====================
// primary // tabs
// ==================== // ====================
// right hand side // dropdown
.nav-dd {
// ==================== .title {
// tabs .label, .icon-caret-down {
display: inline-block;
vertical-align: middle;
}
// ==================== .ui-toggle-dd {
@include transition(rotate .25s ease-in-out .25s);
margin-left: ($baseline/10);
display: inline-block;
vertical-align: middle;
}
// dropdown // dropped down state
&.is-selected {
// ==================== .ui-toggle-dd {
@include transform(rotate(-180deg));
@include transform-origin(50% 50%);
}
}
}
.nav-item {
position: relative;
// &:hover {
}
}
.wrapper-nav-sub {
@include transition (opacity 1.0s ease-in-out 0s);
position: absolute;
top: ($baseline*2.5);
opacity: 0.0;
pointer-events: none;
width: ($baseline*8);
// dropped down state
&.is-shown {
opacity: 1.0;
pointer-events: auto;
}
}
.nav-sub {
@include border-radius(2px);
@include box-sizing(border-box);
@include box-shadow(0 1px 1px $shadow-l1);
position: relative;
width: 100%;
border: 1px solid $gray-l3;
padding: ($baseline/2) ($baseline*0.75);
background: $white;
&:after, &:before {
bottom: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
// ui triangle/nub
&:after {
border-color: rgba(255, 255, 255, 0);
border-bottom-color: $white;
border-width: 10px;
}
&:before {
border-color: rgba(178, 178, 178, 0);
border-bottom-color: $gray-l3;
border-width: 11px;
}
.nav-item {
@extend .t-action3;
display: block;
margin: 0 0 ($baseline/4) 0;
border-bottom: 1px solid $gray-l5;
padding: 0 0($baseline/4) 0;
font-weight: 500;
&:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
a {
display: block;
&:hover, &:active {
color: $blue-s1;
}
}
}
}
// CASE: left-hand side arrow/dd
&.ui-left {
.wrapper-nav-sub {
left: 0;
}
.nav-sub {
text-align: left;
// ui triangle/nub
&:after {
left: $baseline;
margin-left: -10px;
}
&:before {
left: $baseline;
margin-left: -11px;
}
}
}
// CASE: right-hand side arrow/dd
&.ui-right {
.wrapper-nav-sub {
left: none;
right: 0;
}
.nav-sub {
// ui triangle/nub
&:after {
right: $baseline;
margin-right: -10px;
}
&:before {
right: $baseline;
margin-right: -11px;
}
}
}
}
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
@include box-shadow(0 2px 3px $shadow); @include box-shadow(0 2px 3px $shadow);
height: ($baseline*35) !important; height: ($baseline*35) !important;
background: $white !important; background: $white !important;
border: 1px solid $gray; border: 2px solid $blue;
} }
#tender_window { #tender_window {
...@@ -23,11 +23,12 @@ ...@@ -23,11 +23,12 @@
} }
#tender_closer { #tender_closer {
color: $blue-l2 !important; color: $white-t2 !important;
text-transform: uppercase; text-transform: uppercase;
top: 16px !important;
&:hover { &:hover {
color: $blue-l4 !important; color: $white !important;
} }
} }
...@@ -42,15 +43,15 @@ ...@@ -42,15 +43,15 @@
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
} }
.widget-layout .search, .widget-layout .search,
.widget-layout .tabs, .widget-layout .tabs,
.widget-layout .footer, .widget-layout .footer,
.widget-layout .header h1 a { .widget-layout .header h1 a {
display: none; display: none;
} }
.widget-layout .header { .widget-layout .header {
background: rgb(85, 151, 221); background: rgb(0, 159, 230);
padding: 10px 20px; padding: 10px 20px;
} }
...@@ -264,4 +265,4 @@ ...@@ -264,4 +265,4 @@
.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active { .widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active {
background-color: #16ca57; background-color: #16ca57;
color: #fff; color: #fff;
} }
\ No newline at end of file
...@@ -72,14 +72,7 @@ body.index { ...@@ -72,14 +72,7 @@ body.index {
} }
.logo { .logo {
@extend .text-hide; font-weight: 600;
position: relative;
top: 3px;
display: inline-block;
vertical-align: baseline;
width: 282px;
height: 57px;
background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat;
} }
.tagline { .tagline {
......
...@@ -316,6 +316,12 @@ body.course.settings { ...@@ -316,6 +316,12 @@ body.course.settings {
.link-courseURL { .link-courseURL {
@extend .t-copy-lead1; @extend .t-copy-lead1;
@include box-sizing(border-box);
display: block;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover { &:hover {
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<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 Content</small> <small class="subtitle">Content</small>
<span class="sr">&gt; </span>Files &amp; Uploads <span class="sr">&gt; </span>Files &amp; Uploads
</h1> </h1>
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<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 Content</small> <small class="subtitle">Content</small>
<span class="sr">&gt; </span>Course Updates <span class="sr">&gt; </span>Course Updates
</h1> </h1>
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<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 Content</small> <small class="subtitle">Content</small>
<span class="sr">&gt; </span>Static Pages <span class="sr">&gt; </span>Static Pages
</h1> </h1>
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
<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 Content</small> <small class="subtitle">Content</small>
<span class="sr">&gt; </span>Course Outline <span class="sr">&gt; </span>Course Outline
</h1> </h1>
...@@ -165,9 +165,9 @@ ...@@ -165,9 +165,9 @@
<span class="published-status">This section has not been released.</span> <span class="published-status">This section has not been released.</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
%else: %else:
<span class="published-status"><strong>Will Release:</strong> <span class="published-status"><strong>Will Release:</strong>
${date_utils.get_default_time_display(section.lms.start)}</span> ${date_utils.get_default_time_display(section.lms.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" <a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">Edit</a> data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif %endif
</div> </div>
......
...@@ -23,7 +23,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) ...@@ -23,7 +23,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase): class TestCohorts(django.test.TestCase):
@staticmethod @staticmethod
def topic_name_to_id(course, name): def topic_name_to_id(course, name):
""" """
...@@ -34,7 +33,6 @@ class TestCohorts(django.test.TestCase): ...@@ -34,7 +33,6 @@ class TestCohorts(django.test.TestCase):
run=course.url_name, run=course.url_name,
name=name) name=name)
@staticmethod @staticmethod
def config_course_cohorts(course, discussions, def config_course_cohorts(course, discussions,
cohorted, cohorted,
...@@ -80,7 +78,6 @@ class TestCohorts(django.test.TestCase): ...@@ -80,7 +78,6 @@ class TestCohorts(django.test.TestCase):
course.cohort_config = d course.cohort_config = d
def setUp(self): def setUp(self):
""" """
Make sure that course is reloaded every time--clear out the modulestore. Make sure that course is reloaded every time--clear out the modulestore.
...@@ -89,7 +86,6 @@ class TestCohorts(django.test.TestCase): ...@@ -89,7 +86,6 @@ class TestCohorts(django.test.TestCase):
# to course. We don't have a course.clone() method. # to course. We don't have a course.clone() method.
_MODULESTORES.clear() _MODULESTORES.clear()
def test_get_cohort(self): def test_get_cohort(self):
""" """
Make sure get_cohort() does the right thing when the course is cohorted Make sure get_cohort() does the right thing when the course is cohorted
...@@ -105,7 +101,7 @@ class TestCohorts(django.test.TestCase): ...@@ -105,7 +101,7 @@ class TestCohorts(django.test.TestCase):
cohort = CourseUserGroup.objects.create(name="TestCohort", cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course.id, course_id=course.id,
group_type=CourseUserGroup.COHORT) group_type=CourseUserGroup.COHORT)
cohort.users.add(user) cohort.users.add(user)
...@@ -135,7 +131,7 @@ class TestCohorts(django.test.TestCase): ...@@ -135,7 +131,7 @@ class TestCohorts(django.test.TestCase):
cohort = CourseUserGroup.objects.create(name="TestCohort", cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course.id, course_id=course.id,
group_type=CourseUserGroup.COHORT) group_type=CourseUserGroup.COHORT)
# user1 manually added to a cohort # user1 manually added to a cohort
cohort.users.add(user1) cohort.users.add(user1)
...@@ -169,7 +165,6 @@ class TestCohorts(django.test.TestCase): ...@@ -169,7 +165,6 @@ class TestCohorts(django.test.TestCase):
self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup", self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
"user2 should still be in originally placed cohort") "user2 should still be in originally placed cohort")
def test_auto_cohorting_randomization(self): def test_auto_cohorting_randomization(self):
""" """
Make sure get_cohort() randomizes properly. Make sure get_cohort() randomizes properly.
...@@ -199,8 +194,6 @@ class TestCohorts(django.test.TestCase): ...@@ -199,8 +194,6 @@ class TestCohorts(django.test.TestCase):
self.assertGreater(num_users, 1) self.assertGreater(num_users, 1)
self.assertLess(num_users, 50) self.assertLess(num_users, 50)
def test_get_course_cohorts(self): def test_get_course_cohorts(self):
course1_id = 'a/b/c' course1_id = 'a/b/c'
course2_id = 'e/f/g' course2_id = 'e/f/g'
...@@ -214,14 +207,12 @@ class TestCohorts(django.test.TestCase): ...@@ -214,14 +207,12 @@ class TestCohorts(django.test.TestCase):
course_id=course1_id, course_id=course1_id,
group_type=CourseUserGroup.COHORT) group_type=CourseUserGroup.COHORT)
# second course should have no cohorts # second course should have no cohorts
self.assertEqual(get_course_cohorts(course2_id), []) self.assertEqual(get_course_cohorts(course2_id), [])
cohorts = sorted([c.name for c in get_course_cohorts(course1_id)]) cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
self.assertEqual(cohorts, ['TestCohort', 'TestCohort2']) self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
def test_is_commentable_cohorted(self): def test_is_commentable_cohorted(self):
course = modulestore().get_course("edX/toy/2012_Fall") course = modulestore().get_course("edX/toy/2012_Fall")
self.assertFalse(course.is_cohorted) self.assertFalse(course.is_cohorted)
......
...@@ -153,21 +153,36 @@ def click_link(partial_text): ...@@ -153,21 +153,36 @@ def click_link(partial_text):
@world.absorb @world.absorb
def css_text(css_selector): def css_text(css_selector, index=0):
# Wait for the css selector to appear # Wait for the css selector to appear
if world.is_css_present(css_selector): if world.is_css_present(css_selector):
try: try:
return world.browser.find_by_css(css_selector).first.text return world.browser.find_by_css(css_selector)[index].text
except StaleElementReferenceException: except StaleElementReferenceException:
# The DOM was still redrawing. Wait a second and try again. # The DOM was still redrawing. Wait a second and try again.
world.wait(1) world.wait(1)
return world.browser.find_by_css(css_selector).first.text return world.browser.find_by_css(css_selector)[index].text
else: else:
return "" return ""
@world.absorb @world.absorb
def css_html(css_selector, index=0, max_attempts=5):
"""
Returns the HTML of a css_selector and will retry if there is a StaleElementReferenceException
"""
assert is_css_present(css_selector)
attempt = 0
while attempt < max_attempts:
try:
return world.browser.find_by_css(css_selector)[index].html
except:
attempt += 1
return ''
@world.absorb
def css_visible(css_selector): def css_visible(css_selector):
assert is_css_present(css_selector) assert is_css_present(css_selector)
return world.browser.find_by_css(css_selector).visible return world.browser.find_by_css(css_selector).visible
......
from django.db import models from django.db import models
from django.db import models
class TrackingLog(models.Model): class TrackingLog(models.Model):
"""Defines the fields that are stored in the tracking log database"""
dtcreated = models.DateTimeField('creation date', auto_now_add=True) dtcreated = models.DateTimeField('creation date', auto_now_add=True)
username = models.CharField(max_length=32, blank=True) username = models.CharField(max_length=32, blank=True)
ip = models.CharField(max_length=32, blank=True) ip = models.CharField(max_length=32, blank=True)
...@@ -16,6 +15,9 @@ class TrackingLog(models.Model): ...@@ -16,6 +15,9 @@ class TrackingLog(models.Model):
host = models.CharField(max_length=64, blank=True) host = models.CharField(max_length=64, blank=True)
def __unicode__(self): def __unicode__(self):
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source, fmt = (
self.event_type, self.page, self.event) u"[{self.time}] {self.username}@{self.ip}: "
return s u"{self.event_source}| {self.event_type} | "
u"{self.page} | {self.event}"
)
return fmt.format(self=self)
"""Tests for student tracking"""
from django.test import TestCase
from django.core.urlresolvers import reverse, NoReverseMatch
from track.models import TrackingLog
from track.views import user_track
from nose.plugins.skip import SkipTest
class TrackingTest(TestCase):
"""
Tests that tracking logs correctly handle events
"""
def test_post_answers_to_log(self):
"""
Checks that student answer requests submitted to track.views via POST
are correctly logged in the TrackingLog db table
"""
requests = [
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
for request_params in requests:
try: # because /event maps to two different views in lms and cms, we're only going to test lms here
response = self.client.post(reverse(user_track), request_params)
except NoReverseMatch:
raise SkipTest()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, 'success')
tracking_logs = TrackingLog.objects.order_by('-dtcreated')
log = tracking_logs[0]
self.assertEqual(log.event, request_params["event"])
self.assertEqual(log.event_type, request_params["event_type"])
self.assertEqual(log.page, request_params["page"])
def test_get_answers_to_log(self):
"""
Checks that student answer requests submitted to track.views via GET
are correctly logged in the TrackingLog db table
"""
requests = [
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
for request_params in requests:
try: # because /event maps to two different views in lms and cms, we're only going to test lms here
response = self.client.get(reverse(user_track), request_params)
except NoReverseMatch:
raise SkipTest()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, 'success')
tracking_logs = TrackingLog.objects.order_by('-dtcreated')
log = tracking_logs[0]
self.assertEqual(log.event, request_params["event"])
self.assertEqual(log.event_type, request_params["event_type"])
self.assertEqual(log.page, request_params["page"])
...@@ -34,9 +34,10 @@ def log_event(event): ...@@ -34,9 +34,10 @@ def log_event(event):
def user_track(request): def user_track(request):
""" """
Log when GET call to "event" URL is made by a user. Log when POST call to "event" URL is made by a user. Uses request.REQUEST
to allow for GET calls.
GET call should provide "event_type", "event", and "page" arguments. GET or POST call should provide "event_type", "event", and "page" arguments.
""" """
try: # TODO: Do the same for many of the optional META parameters try: # TODO: Do the same for many of the optional META parameters
username = request.user.username username = request.user.username
...@@ -59,13 +60,14 @@ def user_track(request): ...@@ -59,13 +60,14 @@ def user_track(request):
"session": scookie, "session": scookie,
"ip": request.META['REMOTE_ADDR'], "ip": request.META['REMOTE_ADDR'],
"event_source": "browser", "event_source": "browser",
"event_type": request.GET['event_type'], "event_type": request.REQUEST['event_type'],
"event": request.GET['event'], "event": request.REQUEST['event'],
"agent": agent, "agent": agent,
"page": request.GET['page'], "page": request.REQUEST['page'],
"time": datetime.datetime.now(UTC).isoformat(), "time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
} }
log_event(event) log_event(event)
return HttpResponse('success') return HttpResponse('success')
...@@ -92,7 +94,7 @@ def server_track(request, event_type, event, page=None): ...@@ -92,7 +94,7 @@ def server_track(request, event_type, event, page=None):
"page": page, "page": page,
"time": datetime.datetime.now(UTC).isoformat(), "time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
} }
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
return return
...@@ -136,7 +138,7 @@ def task_track(request_info, task_info, event_type, event, page=None): ...@@ -136,7 +138,7 @@ def task_track(request_info, task_info, event_type, event, page=None):
"page": page, "page": page,
"time": datetime.datetime.utcnow().isoformat(), "time": datetime.datetime.utcnow().isoformat(),
"host": request_info.get('host', 'unknown') "host": request_info.get('host', 'unknown')
} }
log_event(event) log_event(event)
......
import re
from django.conf import settings
def can_execute_unsafe_code(course_id):
"""
Determine if this course is allowed to run unsafe code.
For use from the ModuleStore. Checks the `course_id` against a list of whitelisted
regexes.
Returns a boolean, true if the course can run outside the sandbox.
"""
# To decide if we can run unsafe code, we check the course id against
# a list of regexes configured on the server.
for regex in settings.COURSES_WITH_UNSAFE_CODE:
if re.match(regex, course_id):
return True
return False
"""
Tests for sandboxing.py in util app
"""
from django.test import TestCase
from util.sandboxing import can_execute_unsafe_code
from django.test.utils import override_settings
class SandboxingTest(TestCase):
"""
Test sandbox whitelisting
"""
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
def test_sandbox_exclusion(self):
"""
Test to make sure that a non-match returns false
"""
self.assertFalse(can_execute_unsafe_code('edX/notful/empty'))
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
def test_sandbox_inclusion(self):
"""
Test to make sure that a match works across course runs
"""
self.assertTrue(can_execute_unsafe_code('edX/full/2012_Fall'))
self.assertTrue(can_execute_unsafe_code('edX/full/2013_Spring'))
...@@ -107,18 +107,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -107,18 +107,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
""" """
return str(float(answer.values()[0])) return str(float(answer.values()[0]))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, data):
""" """
This is the landing method for AJAX calls. This is the landing method for AJAX calls.
""" """
if dispatch == 'get_hint': if dispatch == 'get_hint':
out = self.get_hint(get) out = self.get_hint(data)
elif dispatch == 'get_feedback': elif dispatch == 'get_feedback':
out = self.get_feedback(get) out = self.get_feedback(data)
elif dispatch == 'vote': elif dispatch == 'vote':
out = self.tally_vote(get) out = self.tally_vote(data)
elif dispatch == 'submit_hint': elif dispatch == 'submit_hint':
out = self.submit_hint(get) out = self.submit_hint(data)
else: else:
return json.dumps({'contents': 'Error - invalid operation.'}) return json.dumps({'contents': 'Error - invalid operation.'})
...@@ -128,16 +128,16 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -128,16 +128,16 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
out.update({'op': dispatch}) out.update({'op': dispatch})
return json.dumps({'contents': self.system.render_template('hinter_display.html', out)}) return json.dumps({'contents': self.system.render_template('hinter_display.html', out)})
def get_hint(self, get): def get_hint(self, data):
""" """
The student got the incorrect answer found in get. Give him a hint. The student got the incorrect answer found in data. Give him a hint.
Called by hinter javascript after a problem is graded as incorrect. Called by hinter javascript after a problem is graded as incorrect.
Args: Args:
`get` -- must be interpretable by capa_answer_to_str. `data` -- must be interpretable by capa_answer_to_str.
Output keys: Output keys:
- 'best_hint' is the hint text with the most votes. - 'best_hint' is the hint text with the most votes.
- 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `get`. - 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `data`.
- 'answer' is the parsed answer that was submitted. - 'answer' is the parsed answer that was submitted.
""" """
try: try:
...@@ -181,12 +181,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -181,12 +181,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
'rand_hint_2': rand_hint_2, 'rand_hint_2': rand_hint_2,
'answer': answer} 'answer': answer}
def get_feedback(self, get): def get_feedback(self, data):
""" """
The student got it correct. Ask him to vote on hints, or submit a hint. The student got it correct. Ask him to vote on hints, or submit a hint.
Args: Args:
`get` -- not actually used. (It is assumed that the answer is correct.) `data` -- not actually used. (It is assumed that the answer is correct.)
Output keys: Output keys:
- 'index_to_hints' maps previous answer indices to hints that the user saw earlier. - 'index_to_hints' maps previous answer indices to hints that the user saw earlier.
- 'index_to_answer' maps previous answer indices to the actual answer submitted. - 'index_to_answer' maps previous answer indices to the actual answer submitted.
...@@ -221,20 +221,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -221,20 +221,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return {'index_to_hints': index_to_hints, 'index_to_answer': index_to_answer} return {'index_to_hints': index_to_hints, 'index_to_answer': index_to_answer}
def tally_vote(self, get): def tally_vote(self, data):
""" """
Tally a user's vote on his favorite hint. Tally a user's vote on his favorite hint.
Args: Args:
`get` -- expected to have the following keys: `data` -- expected to have the following keys:
'answer': ans_no (index in previous_answers) 'answer': ans_no (index in previous_answers)
'hint': hint_pk 'hint': hint_pk
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs. Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
""" """
if self.user_voted: if self.user_voted:
return {} return {}
ans_no = int(get['answer']) ans_no = int(data['answer'])
hint_no = str(get['hint']) hint_no = str(data['hint'])
answer = self.previous_answers[ans_no][0] answer = self.previous_answers[ans_no][0]
# We use temp_dict because we need to do a direct write for the database to update. # We use temp_dict because we need to do a direct write for the database to update.
temp_dict = self.hints temp_dict = self.hints
...@@ -254,19 +254,19 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -254,19 +254,19 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
self.previous_answers = [] self.previous_answers = []
return {'hint_and_votes': hint_and_votes} return {'hint_and_votes': hint_and_votes}
def submit_hint(self, get): def submit_hint(self, data):
""" """
Take a hint submission and add it to the database. Take a hint submission and add it to the database.
Args: Args:
`get` -- expected to have the following keys: `data` -- expected to have the following keys:
'answer': answer index in previous_answers 'answer': answer index in previous_answers
'hint': text of the new hint that the user is adding 'hint': text of the new hint that the user is adding
Returns a thank-you message. Returns a thank-you message.
""" """
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well. # Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint = escape(get['hint']) hint = escape(data['hint'])
answer = self.previous_answers[int(get['answer'])][0] answer = self.previous_answers[int(data['answer'])][0]
# Only allow a student to vote or submit a hint once. # Only allow a student to vote or submit a hint once.
if self.user_voted: if self.user_voted:
return {'message': 'Sorry, but you have already voted!'} return {'message': 'Sorry, but you have already voted!'}
......
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
#answer-tabs .ui-widget-header { #answer-tabs .ui-widget-header {
border-bottom: 1px solid #DCDCDC; border-bottom: 1px solid #DCDCDC;
background: #F3F3F3; background: #FDF8EB;
} }
#answer-tabs .ui-tabs-nav .ui-state-default { #answer-tabs .ui-tabs-nav .ui-state-default {
border: 1px solid #DCDCDC; border: 1px solid #DCDCDC;
background: #F8F8F8; background: #E6E6E3;
margin-bottom: 0px; margin-bottom: 0px;
} }
......
...@@ -166,7 +166,6 @@ nav.sequence-nav { ...@@ -166,7 +166,6 @@ nav.sequence-nav {
p { p {
background: #333; background: #333;
color: #fff; color: #fff;
display: none;
font-family: $sans-serif; font-family: $sans-serif;
line-height: lh(); line-height: lh();
left: 0px; left: 0px;
......
...@@ -111,7 +111,15 @@ class @Sequence ...@@ -111,7 +111,15 @@ class @Sequence
if (1 <= new_position) and (new_position <= @num_contents) if (1 <= new_position) and (new_position <= @num_contents)
Logger.log "seq_goto", old: @position, new: new_position, id: @id Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread analytics.pageview @id
# navigation by clicking the tab directly
analytics.track "Accessed Sequential Directly",
sequence_id: @id
current_sequential: @position
target_sequential: new_position
# On Sequence change, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee # for queued submissions, see ../capa/display.coffee
if window.queuePollerID if window.queuePollerID
window.clearTimeout(window.queuePollerID) window.clearTimeout(window.queuePollerID)
...@@ -125,12 +133,30 @@ class @Sequence ...@@ -125,12 +133,30 @@ class @Sequence
event.preventDefault() event.preventDefault()
new_position = @position + 1 new_position = @position + 1
Logger.log "seq_next", old: @position, new: new_position, id: @id Logger.log "seq_next", old: @position, new: new_position, id: @id
analytics.pageview @id
# navigation using the next arrow
analytics.track "Accessed Next Sequential",
sequence_id: @id
current_sequential: @position
target_sequential: new_position
@render new_position @render new_position
previous: (event) => previous: (event) =>
event.preventDefault() event.preventDefault()
new_position = @position - 1 new_position = @position - 1
Logger.log "seq_prev", old: @position, new: new_position, id: @id Logger.log "seq_prev", old: @position, new: new_position, id: @id
analytics.pageview @id
# navigation using the previous arrow
analytics.track "Accessed Previous Sequential",
sequence_id: @id
current_sequential: @position
target_sequential: new_position
@render new_position @render new_position
link_for: (position) -> link_for: (position) ->
......
"""
Provide names as exported by older mongo.py module
"""
from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore, MongoUsage from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore, MongoUsage
# Backwards compatibility for prod systems that refererence # Backwards compatibility for prod systems that refererence
......
"""
Modulestore backed by Mongodb.
Stores individual XModules as single documents with the following
structure:
{
'_id': <location.as_dict>,
'metadata': <dict containing all Scope.settings fields>
'definition': <dict containing all Scope.content fields>
'definition.children': <list of all child location.url()s>
}
"""
import pymongo import pymongo
import sys import sys
import logging import logging
...@@ -19,8 +33,7 @@ from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError ...@@ -19,8 +33,7 @@ from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
from xblock.core import Scope from xblock.core import Scope
from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son
from xmodule.modulestore.exceptions import (ItemNotFoundError, from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateItemError
DuplicateItemError)
from xmodule.modulestore.inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata from xmodule.modulestore.inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -32,6 +45,7 @@ log = logging.getLogger(__name__) ...@@ -32,6 +45,7 @@ log = logging.getLogger(__name__)
def get_course_id_no_run(location): def get_course_id_no_run(location):
''' '''
Return the first two components of the course_id for this location (org/course)
''' '''
return "/".join([location.org, location.course]) return "/".join([location.org, location.course])
...@@ -615,6 +629,9 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -615,6 +629,9 @@ class MongoModuleStore(ModuleStoreBase):
return item return item
def fire_updated_modulestore_signal(self, course_id, location): def fire_updated_modulestore_signal(self, course_id, location):
"""
Send a signal using `self.modulestore_update_signal`, if that has been set
"""
if self.modulestore_update_signal is not None: if self.modulestore_update_signal is not None:
self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id, self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id,
location=location) location=location)
...@@ -758,5 +775,3 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -758,5 +775,3 @@ class MongoModuleStore(ModuleStoreBase):
are loaded on demand, rather than up front are loaded on demand, rather than up front
""" """
return {} return {}
"""
A ModuleStore that knows about a special version 'draft'. Modules
marked as 'draft' are read in preference to modules without the 'draft'
version by this ModuleStore (so, access to i4x://org/course/cat/name
returns the i4x://org/course/cat/name@draft object if that exists,
and otherwise returns i4x://org/course/cat/name).
"""
from datetime import datetime from datetime import datetime
from xmodule.modulestore import Location, namedtuple_to_son from xmodule.modulestore import Location, namedtuple_to_son
...@@ -217,7 +225,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -217,7 +225,6 @@ class DraftModuleStore(MongoModuleStore):
def _query_children_for_cache_children(self, items): def _query_children_for_cache_children(self, items):
# first get non-draft in a round-trip # first get non-draft in a round-trip
queried_children = []
to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items) to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items)
to_process_dict = {} to_process_dict = {}
...@@ -243,7 +250,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -243,7 +250,6 @@ class DraftModuleStore(MongoModuleStore):
to_process_dict[draft_as_non_draft_loc] = draft to_process_dict[draft_as_non_draft_loc] = draft
# convert the dict - which is used for look ups - back into a list # convert the dict - which is used for look ups - back into a list
for key, value in to_process_dict.iteritems(): queried_children = to_process_dict.values()
queried_children.append(value)
return queried_children return queried_children
...@@ -19,14 +19,13 @@ class XModuleCourseFactory(Factory): ...@@ -19,14 +19,13 @@ class XModuleCourseFactory(Factory):
ABSTRACT_FACTORY = True ABSTRACT_FACTORY = True
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, **kwargs):
template = Location('i4x', 'edx', 'templates', 'course', 'Empty') template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.get('org') org = kwargs.pop('org', None)
number = kwargs.get('number') number = kwargs.pop('number', None)
display_name = kwargs.get('display_name') display_name = kwargs.pop('display_name', None)
location = Location('i4x', org, number, location = Location('i4x', org, number, 'course', Location.clean(display_name))
'course', Location.clean(display_name))
try: try:
store = modulestore('direct') store = modulestore('direct')
...@@ -41,7 +40,7 @@ class XModuleCourseFactory(Factory): ...@@ -41,7 +40,7 @@ class XModuleCourseFactory(Factory):
new_course.display_name = display_name new_course.display_name = display_name
new_course.lms.start = datetime.datetime.now(UTC) new_course.lms.start = datetime.datetime.now(UTC)
new_course.tabs = kwargs.get( new_course.tabs = kwargs.pop(
'tabs', 'tabs',
[ [
{"type": "courseware"}, {"type": "courseware"},
...@@ -51,14 +50,14 @@ class XModuleCourseFactory(Factory): ...@@ -51,14 +50,14 @@ class XModuleCourseFactory(Factory):
{"type": "progress", "name": "Progress"} {"type": "progress", "name": "Progress"}
] ]
) )
new_course.discussion_link = kwargs.get('discussion_link')
# Update the data in the mongo datastore # The rest of kwargs become attributes on the course:
store.update_metadata(new_course.location.url(), own_metadata(new_course)) for k, v in kwargs.iteritems():
setattr(new_course, k, v)
data = kwargs.get('data') # Update the data in the mongo datastore
if data is not None: store.update_metadata(new_course.location, own_metadata(new_course))
store.update_item(new_course.location, data) store.update_item(new_course.location, new_course._model_data._kvs._data)
# update_item updates the the course as it exists in the modulestore, but doesn't # update_item updates the the course as it exists in the modulestore, but doesn't
# update the instance we are working with, so have to refetch the course after updating it. # update the instance we are working with, so have to refetch the course after updating it.
...@@ -101,7 +100,7 @@ class XModuleItemFactory(Factory): ...@@ -101,7 +100,7 @@ class XModuleItemFactory(Factory):
return parent._replace(category=attr.category, name=dest_name) return parent._replace(category=attr.category, name=dest_name)
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, **kwargs):
""" """
Uses *kwargs*: Uses *kwargs*:
......
"""
Methods for exporting course data to XML
"""
import logging import logging
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from fs.osfs import OSFS from fs.osfs import OSFS
from json import dumps from json import dumps
import json import json
from json.encoder import JSONEncoder
import datetime import datetime
class EdxJSONEncoder(json.JSONEncoder): class EdxJSONEncoder(json.JSONEncoder):
"""
Custom JSONEncoder that handles `Location` and `datetime.datetime` objects.
`Location`s are encoded as their url string form, and `datetime`s as
ISO date strings
"""
def default(self, obj): def default(self, obj):
if isinstance(obj, Location): if isinstance(obj, Location):
return obj.url() return obj.url()
...@@ -22,7 +32,19 @@ class EdxJSONEncoder(json.JSONEncoder): ...@@ -22,7 +32,19 @@ class EdxJSONEncoder(json.JSONEncoder):
else: else:
return super(EdxJSONEncoder, self).default(obj) return super(EdxJSONEncoder, self).default(obj)
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore=None): def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore=None):
"""
Export all modules from `modulestore` and content from `contentstore` as xml to `root_dir`.
`modulestore`: A `ModuleStore` object that is the source of the modules to export
`contentstore`: A `ContentStore` object that is the source of the content to export
`course_location`: The `Location` of the `CourseModuleDescriptor` to export
`root_dir`: The directory to write the exported xml to
`course_dir`: The name of the directory inside `root_dir` to write the course content to
`draft_modulestore`: An optional `DraftModuleStore` that contains draft content, which will be exported
alongside the public content in the course.
"""
course = modulestore.get_item(course_location) course = modulestore.get_item(course_location)
......
# Tests for xmodule.util.date_utils """Tests for xmodule.util.date_utils"""
from nose.tools import assert_equals, assert_false from nose.tools import assert_equals, assert_false
from xmodule.util.date_utils import get_default_time_display, almost_same_datetime from xmodule.util.date_utils import get_default_time_display, almost_same_datetime
...@@ -19,6 +19,7 @@ def test_get_default_time_display(): ...@@ -19,6 +19,7 @@ def test_get_default_time_display():
"Mar 12, 1992 at 15:03", "Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False)) get_default_time_display(test_time, False))
def test_get_default_time_display_notz(): def test_get_default_time_display_notz():
test_time = datetime(1992, 3, 12, 15, 3, 30) test_time = datetime(1992, 3, 12, 15, 3, 30)
assert_equals( assert_equals(
...@@ -31,8 +32,10 @@ def test_get_default_time_display_notz(): ...@@ -31,8 +32,10 @@ def test_get_default_time_display_notz():
"Mar 12, 1992 at 15:03", "Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False)) get_default_time_display(test_time, False))
# pylint: disable=W0232 # pylint: disable=W0232
class NamelessTZ(tzinfo): class NamelessTZ(tzinfo):
"""Static timezone for testing"""
def utcoffset(self, _dt): def utcoffset(self, _dt):
return timedelta(hours=-3) return timedelta(hours=-3)
...@@ -40,6 +43,7 @@ class NamelessTZ(tzinfo): ...@@ -40,6 +43,7 @@ class NamelessTZ(tzinfo):
def dst(self, _dt): def dst(self, _dt):
return timedelta(0) return timedelta(0)
def test_get_default_time_display_no_tzname(): def test_get_default_time_display_no_tzname():
assert_equals("", get_default_time_display(None)) assert_equals("", get_default_time_display(None))
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ()) test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ())
...@@ -53,6 +57,7 @@ def test_get_default_time_display_no_tzname(): ...@@ -53,6 +57,7 @@ def test_get_default_time_display_no_tzname():
"Mar 12, 1992 at 15:03", "Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False)) get_default_time_display(test_time, False))
def test_almost_same_datetime(): def test_almost_same_datetime():
assert almost_same_datetime( assert almost_same_datetime(
datetime(2013, 5, 3, 10, 20, 30), datetime(2013, 5, 3, 10, 20, 30),
......
"""
Tests of XML export
"""
import unittest import unittest
import pytz import pytz
from datetime import datetime, timedelta, tzinfo from datetime import datetime, timedelta, tzinfo
from fs.osfs import OSFS from fs.osfs import OSFS
from mock import Mock
from path import path from path import path
from tempfile import mkdtemp from tempfile import mkdtemp
import shutil import shutil
...@@ -136,19 +139,22 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -136,19 +139,22 @@ class RoundTripTestCase(unittest.TestCase):
class TestEdxJsonEncoder(unittest.TestCase): class TestEdxJsonEncoder(unittest.TestCase):
"""
Tests for xml_exporter.EdxJSONEncoder
"""
def setUp(self): def setUp(self):
self.encoder = EdxJSONEncoder() self.encoder = EdxJSONEncoder()
class OffsetTZ(tzinfo): class OffsetTZ(tzinfo):
"""A timezone with non-None utcoffset""" """A timezone with non-None utcoffset"""
def utcoffset(self, dt): def utcoffset(self, _dt):
return timedelta(hours=4) return timedelta(hours=4)
self.offset_tz = OffsetTZ() self.offset_tz = OffsetTZ()
class NullTZ(tzinfo): class NullTZ(tzinfo):
"""A timezone with None as its utcoffset""" """A timezone with None as its utcoffset"""
def utcoffset(self, dt): def utcoffset(self, _dt):
return None return None
self.null_utc_tz = NullTZ() self.null_utc_tz = NullTZ()
......
"""
Convenience methods for working with datetime objects
"""
import datetime import datetime
def get_default_time_display(dt, show_timezone=True): def get_default_time_display(dt, show_timezone=True):
""" """
......
...@@ -3,20 +3,10 @@ describe 'Logger', -> ...@@ -3,20 +3,10 @@ describe 'Logger', ->
expect(window.log_event).toBe Logger.log expect(window.log_event).toBe Logger.log
describe 'log', -> describe 'log', ->
it 'sends an event to Segment.io, if the event is whitelisted and the data is not a dictionary', ->
spyOn(analytics, 'track')
Logger.log 'seq_goto', 'data'
expect(analytics.track).toHaveBeenCalledWith 'seq_goto', value: 'data'
it 'sends an event to Segment.io, if the event is whitelisted and the data is a dictionary', ->
spyOn(analytics, 'track')
Logger.log 'seq_goto', value: 'data'
expect(analytics.track).toHaveBeenCalledWith 'seq_goto', value: 'data'
it 'send a request to log event', -> it 'send a request to log event', ->
spyOn $, 'getWithPrefix' spyOn $, 'postWithPrefix'
Logger.log 'example', 'data' Logger.log 'example', 'data'
expect($.getWithPrefix).toHaveBeenCalledWith '/event', expect($.postWithPrefix).toHaveBeenCalledWith '/event',
event_type: 'example' event_type: 'example'
event: '"data"' event: '"data"'
page: window.location.href page: window.location.href
......
class @Logger class @Logger
# events we want sent to Segment.io for tracking
SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev", "problem_check", "problem_reset", "problem_show", "problem_save"]
# listeners[event_type][element] -> list of callbacks # listeners[event_type][element] -> list of callbacks
listeners = {} listeners = {}
@log: (event_type, data, element = null) -> @log: (event_type, data, element = null) ->
# Segment.io event tracking
if event_type in SEGMENT_IO_WHITELIST
# to avoid changing the format of data sent to our servers, we only massage it here
if typeof data isnt 'object' or data is null
analytics.track event_type, value: data
else
analytics.track event_type, data
# Check to see if we're listening for the event type. # Check to see if we're listening for the event type.
if event_type of listeners if event_type of listeners
# Cool. Do the elements also match? # Cool. Do the elements also match?
...@@ -28,7 +17,7 @@ class @Logger ...@@ -28,7 +17,7 @@ class @Logger
callback(event_type, data, element) callback(event_type, data, element)
# Regardless of whether any callbacks were made, log this event. # Regardless of whether any callbacks were made, log this event.
$.getWithPrefix '/event', $.postWithPrefix '/event',
event_type: event_type event_type: event_type
event: JSON.stringify(data) event: JSON.stringify(data)
page: window.location.href page: window.location.href
...@@ -43,7 +32,6 @@ class @Logger ...@@ -43,7 +32,6 @@ class @Logger
else else
listeners[event_type][element].push callback listeners[event_type][element].push callback
@bind: -> @bind: ->
window.onunload = -> window.onunload = ->
$.ajaxWithPrefix $.ajaxWithPrefix
...@@ -54,5 +42,5 @@ class @Logger ...@@ -54,5 +42,5 @@ class @Logger
page: window.location.href page: window.location.href
async: false async: false
# Keeping this for conpatibility issue only. # Keeping this for compatibility issue only.
@log_event = Logger.log @log_event = Logger.log
...@@ -64,6 +64,12 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -64,6 +64,12 @@ You should be familiar with the following. If you're not, go read some docs...
from a Location object, and the ModuleSystem knows how to render things, from a Location object, and the ModuleSystem knows how to render things,
track events, and complain about 404s track events, and complain about 404s
- XModules and XModuleDescriptors are uniquely identified by a Location object, encoding the organization, course, category, name, and possibly revision of the module.
- XModule initialization: XModules are instantiated by the `XModuleDescriptor.xmodule` method, and given a ModuleSystem, the descriptor which instantiated it, and their relevant model data.
- XModuleDescriptor initialization: If an XModuleDescriptor is loaded from an XML-based course, the XML data is passed into its `from_xml` method, which is responsible for instantiating a descriptor with the correct attributes. If it's in Mongo, the descriptor is instantiated directly. The module's attributes will be present in the `model_data` dict.
- `course.xml` format. We use python setuptools to connect supported tags with the descriptors that handle them. See `common/lib/xmodule/setup.py`. There are checking and validation tools in `common/validate`. - `course.xml` format. We use python setuptools to connect supported tags with the descriptors that handle them. See `common/lib/xmodule/setup.py`. There are checking and validation tools in `common/validate`.
- the xml import+export functionality is in `xml_module.py:XmlDescriptor`, which is a mixin class that's used by the actual descriptor classes. - the xml import+export functionality is in `xml_module.py:XmlDescriptor`, which is a mixin class that's used by the actual descriptor classes.
......
...@@ -37,7 +37,7 @@ from courseware.access import has_access ...@@ -37,7 +37,7 @@ from courseware.access import has_access
from courseware.masquerade import setup_masquerade from courseware.masquerade import setup_masquerade
from courseware.model_data import LmsKeyValueStore, LmsUsage, ModelDataCache from courseware.model_data import LmsKeyValueStore, LmsUsage, ModelDataCache
from courseware.models import StudentModule from courseware.models import StudentModule
from util.sandboxing import can_execute_unsafe_code
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -61,9 +61,9 @@ def make_track_function(request): ...@@ -61,9 +61,9 @@ def make_track_function(request):
''' '''
import track.views import track.views
def f(event_type, event): def function(event_type, event):
return track.views.server_track(request, event_type, event, page='x_module') return track.views.server_track(request, event_type, event, page='x_module')
return f return function
def toc_for_course(user, request, course, active_chapter, active_section, model_data_cache): def toc_for_course(user, request, course, active_chapter, active_section, model_data_cache):
...@@ -171,9 +171,9 @@ def get_xqueue_callback_url_prefix(request): ...@@ -171,9 +171,9 @@ def get_xqueue_callback_url_prefix(request):
should go back to the LMS, not to the worker. should go back to the LMS, not to the worker.
""" """
prefix = '{proto}://{host}'.format( prefix = '{proto}://{host}'.format(
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'), proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'),
host=request.get_host() host=request.get_host()
) )
return settings.XQUEUE_INTERFACE.get('callback_url', prefix) return settings.XQUEUE_INTERFACE.get('callback_url', prefix)
...@@ -313,14 +313,6 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -313,14 +313,6 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
statsd.increment("lms.courseware.question_answered", tags=tags) statsd.increment("lms.courseware.question_answered", tags=tags)
def can_execute_unsafe_code():
# To decide if we can run unsafe code, we check the course id against
# a list of regexes configured on the server.
for regex in settings.COURSES_WITH_UNSAFE_CODE:
if re.match(regex, course_id):
return True
return False
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from # that the xml was loaded from
...@@ -348,7 +340,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -348,7 +340,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
open_ended_grading_interface=open_ended_grading_interface, open_ended_grading_interface=open_ended_grading_interface,
s3_interface=s3_interface, s3_interface=s3_interface,
cache=cache, cache=cache,
can_execute_unsafe_code=can_execute_unsafe_code, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
) )
# pass position specified in URL to module through ModuleSystem # pass position specified in URL to module through ModuleSystem
system.set('position', position) system.set('position', position)
......
...@@ -32,13 +32,11 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -32,13 +32,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
1. TEMPLATE_NAME 1. TEMPLATE_NAME
2. DATA 2. DATA
3. MODEL_DATA 3. MODEL_DATA
4. COURSE_DATA and USER_COUNT if needed
This class should not contain any tests, because TEMPLATE_NAME This class should not contain any tests, because TEMPLATE_NAME
should be defined in child class. should be defined in child class.
""" """
USER_COUNT = 2 USER_COUNT = 2
COURSE_DATA = {}
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
TEMPLATE_NAME = "" TEMPLATE_NAME = ""
...@@ -47,7 +45,7 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -47,7 +45,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
def setUp(self): def setUp(self):
self.course = CourseFactory.create(data=self.COURSE_DATA) self.course = CourseFactory.create()
# Turn off cache. # Turn off cache.
modulestore().request_cache = None modulestore().request_cache = None
......
...@@ -3,7 +3,6 @@ import datetime ...@@ -3,7 +3,6 @@ import datetime
from django.test import TestCase from django.test import TestCase
from django.http import Http404 from django.http import Http404
from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import RequestFactory from django.test.client import RequestFactory
...@@ -52,8 +51,8 @@ class ViewsTestCase(TestCase): ...@@ -52,8 +51,8 @@ class ViewsTestCase(TestCase):
self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC) self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC)
self.course_id = 'edX/toy/2012_Fall' self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user, self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id, course_id=self.course_id,
created=self.date)[0] created=self.date)[0]
self.location = ['tag', 'org', 'course', 'category', 'name'] self.location = ['tag', 'org', 'course', 'category', 'name']
self._MODULESTORES = {} self._MODULESTORES = {}
# This is a CourseDescriptor object # This is a CourseDescriptor object
......
...@@ -6,10 +6,8 @@ Unit tests for enrollment methods in views.py ...@@ -6,10 +6,8 @@ Unit tests for enrollment methods in views.py
from django.test.utils import override_settings from django.test.utils import override_settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......
...@@ -27,11 +27,11 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -27,11 +27,11 @@ class TestGradebook(ModuleStoreTestCase):
modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None
course_data = {} kwargs = {}
if self.grading_policy is not None: if self.grading_policy is not None:
course_data['grading_policy'] = self.grading_policy kwargs['grading_policy'] = self.grading_policy
self.course = CourseFactory.create(data=course_data) self.course = CourseFactory.create(**kwargs)
chapter = ItemFactory.create( chapter = ItemFactory.create(
parent_location=self.course.location, parent_location=self.course.location,
template="i4x://edx/templates/sequential/Empty", template="i4x://edx/templates/sequential/Empty",
......
"""
Test the lms/staticbook views.
"""
import textwrap
import mock
import requests
from django.test.utils import override_settings
from django.core.urlresolvers import reverse, NoReverseMatch
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
IMAGE_BOOK = ("An Image Textbook", "http://example.com/the_book/")
PDF_BOOK = {
"tab_title": "Textbook",
"title": "A PDF Textbook",
"chapters": [
{ "title": "Chapter 1 for PDF", "url": "https://somehost.com/the_book/chap1.pdf" },
{ "title": "Chapter 2 for PDF", "url": "https://somehost.com/the_book/chap2.pdf" },
],
}
HTML_BOOK = {
"tab_title": "Textbook",
"title": "An HTML Textbook",
"chapters": [
{ "title": "Chapter 1 for HTML", "url": "https://somehost.com/the_book/chap1.html" },
{ "title": "Chapter 2 for HTML", "url": "https://somehost.com/the_book/chap2.html" },
],
}
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class StaticBookTest(ModuleStoreTestCase):
"""
Helpers for the static book tests.
"""
def __init__(self, *args, **kwargs):
super(StaticBookTest, self).__init__(*args, **kwargs)
self.course = None
def make_course(self, **kwargs):
"""
Make a course with an enrolled logged-in student.
"""
self.course = CourseFactory.create(**kwargs)
user = UserFactory.create()
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
self.client.login(username=user.username, password='test')
def make_url(self, url_name, **kwargs):
"""
Make a URL for a `url_name` using keyword args for url slots.
Automatically provides the course id.
"""
kwargs['course_id'] = self.course.id
url = reverse(url_name, kwargs=kwargs)
return url
class StaticImageBookTest(StaticBookTest):
"""
Test the image-based static book view.
"""
def test_book(self):
# We can access a book.
with mock.patch.object(requests, 'get') as mock_get:
mock_get.return_value.text = textwrap.dedent('''\
<?xml version="1.0"?>
<table_of_contents>
<entry page="9" page_label="ix" name="Contents!?"/>
<entry page="1" page_label="i" name="Preamble">
<entry page="4" page_label="iv" name="About the Elephants"/>
</entry>
</table_of_contents>
''')
self.make_course(textbooks=[IMAGE_BOOK])
url = self.make_url('book', book_index=0)
response = self.client.get(url)
self.assertContains(response, "Contents!?")
self.assertContains(response, "About the Elephants")
def test_bad_book_id(self):
# A bad book id will be a 404.
self.make_course(textbooks=[IMAGE_BOOK])
with self.assertRaises(NoReverseMatch):
self.make_url('book', book_index='fooey')
def test_out_of_range_book_id(self):
self.make_course()
url = self.make_url('book', book_index=0)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
class StaticPdfBookTest(StaticBookTest):
"""
Test the PDF static book view.
"""
def test_book(self):
# We can access a book.
self.make_course(pdf_textbooks=[PDF_BOOK])
url = self.make_url('pdf_book', book_index=0)
response = self.client.get(url)
self.assertContains(response, "Chapter 1 for PDF")
self.assertNotContains(response, "options.chapterNum =")
self.assertNotContains(response, "options.pageNum =")
def test_book_chapter(self):
# We can access a book at a particular chapter.
self.make_course(pdf_textbooks=[PDF_BOOK])
url = self.make_url('pdf_book', book_index=0, chapter=2)
response = self.client.get(url)
self.assertContains(response, "Chapter 2 for PDF")
self.assertContains(response, "options.chapterNum = 2;")
self.assertNotContains(response, "options.pageNum =")
def test_book_page(self):
# We can access a book at a particular page.
self.make_course(pdf_textbooks=[PDF_BOOK])
url = self.make_url('pdf_book', book_index=0, page=17)
response = self.client.get(url)
self.assertContains(response, "Chapter 1 for PDF")
self.assertNotContains(response, "options.chapterNum =")
self.assertContains(response, "options.pageNum = 17;")
def test_book_chapter_page(self):
# We can access a book at a particular chapter and page.
self.make_course(pdf_textbooks=[PDF_BOOK])
url = self.make_url('pdf_book', book_index=0, chapter=2, page=17)
response = self.client.get(url)
self.assertContains(response, "Chapter 2 for PDF")
self.assertContains(response, "options.chapterNum = 2;")
self.assertContains(response, "options.pageNum = 17;")
def test_bad_book_id(self):
# If the book id isn't an int, we'll get a 404.
self.make_course(pdf_textbooks=[PDF_BOOK])
with self.assertRaises(NoReverseMatch):
self.make_url('pdf_book', book_index='fooey', chapter=1)
def test_out_of_range_book_id(self):
# If we have one book, asking for the second book will fail with a 404.
self.make_course(pdf_textbooks=[PDF_BOOK])
url = self.make_url('pdf_book', book_index=1, chapter=1)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_no_book(self):
# If we have no books, asking for the first book will fail with a 404.
self.make_course()
url = self.make_url('pdf_book', book_index=0, chapter=1)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_chapter_xss(self):
# The chapter in the URL used to go right on the page.
self.make_course(pdf_textbooks=[PDF_BOOK])
# It's no longer possible to use a non-integer chapter.
with self.assertRaises(NoReverseMatch):
self.make_url('pdf_book', book_index=0, chapter='xyzzy')
def test_page_xss(self):
# The page in the URL used to go right on the page.
self.make_course(pdf_textbooks=[PDF_BOOK])
# It's no longer possible to use a non-integer page.
with self.assertRaises(NoReverseMatch):
self.make_url('pdf_book', book_index=0, page='xyzzy')
def test_chapter_page_xss(self):
# The page in the URL used to go right on the page.
self.make_course(pdf_textbooks=[PDF_BOOK])
# It's no longer possible to use a non-integer page and a non-integer chapter.
with self.assertRaises(NoReverseMatch):
self.make_url('pdf_book', book_index=0, chapter='fooey', page='xyzzy')
class StaticHtmlBookTest(StaticBookTest):
"""
Test the HTML static book view.
"""
def test_book(self):
# We can access a book.
self.make_course(html_textbooks=[HTML_BOOK])
url = self.make_url('html_book', book_index=0)
response = self.client.get(url)
self.assertContains(response, "Chapter 1 for HTML")
self.assertNotContains(response, "options.chapterNum =")
def test_book_chapter(self):
# We can access a book at a particular chapter.
self.make_course(html_textbooks=[HTML_BOOK])
url = self.make_url('html_book', book_index=0, chapter=2)
response = self.client.get(url)
self.assertContains(response, "Chapter 2 for HTML")
self.assertContains(response, "options.chapterNum = 2;")
def test_bad_book_id(self):
# If we have one book, asking for the second book will fail with a 404.
self.make_course(html_textbooks=[HTML_BOOK])
url = self.make_url('html_book', book_index=1, chapter=1)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_no_book(self):
# If we have no books, asking for the first book will fail with a 404.
self.make_course()
url = self.make_url('html_book', book_index=0, chapter=1)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_chapter_xss(self):
# The chapter in the URL used to go right on the page.
self.make_course(pdf_textbooks=[HTML_BOOK])
# It's no longer possible to use a non-integer chapter.
with self.assertRaises(NoReverseMatch):
self.make_url('html_book', book_index=0, chapter='xyzzy')
"""
Views for serving static textbooks.
"""
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
...@@ -10,6 +14,9 @@ from static_replace import replace_static_urls ...@@ -10,6 +14,9 @@ from static_replace import replace_static_urls
@login_required @login_required
def index(request, course_id, book_index, page=None): def index(request, course_id, book_index, page=None):
"""
Serve static image-based textbooks.
"""
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
...@@ -22,18 +29,31 @@ def index(request, course_id, book_index, page=None): ...@@ -22,18 +29,31 @@ def index(request, course_id, book_index, page=None):
if page is None: if page is None:
page = textbook.start_page page = textbook.start_page
return render_to_response('staticbook.html', return render_to_response(
{'book_index': book_index, 'page': int(page), 'staticbook.html',
'course': course, {
'book_url': textbook.book_url, 'book_index': book_index, 'page': int(page),
'table_of_contents': table_of_contents, 'course': course,
'start_page': textbook.start_page, 'book_url': textbook.book_url,
'end_page': textbook.end_page, 'table_of_contents': table_of_contents,
'staff_access': staff_access}) 'start_page': textbook.start_page,
'end_page': textbook.end_page,
'staff_access': staff_access,
def index_shifted(request, course_id, page): },
return index(request, course_id=course_id, page=int(page) + 24) )
def remap_static_url(original_url, course):
"""Remap a URL in the ways the course requires."""
# Ick: this should be possible without having to quote and unquote the URL...
input_url = "'" + original_url + "'"
output_url = replace_static_urls(
input_url,
getattr(course, 'data_dir', None),
course_namespace=course.location,
)
# strip off the quotes again...
return output_url[1:-1]
@login_required @login_required
...@@ -60,16 +80,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -60,16 +80,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
raise Http404("Invalid book index value: {0}".format(book_index)) raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.pdf_textbooks[book_index] textbook = course.pdf_textbooks[book_index]
def remap_static_url(original_url, course):
input_url = "'" + original_url + "'"
output_url = replace_static_urls(
input_url,
getattr(course, 'data_dir', None),
course_namespace=course.location
)
# strip off the quotes again...
return output_url[1:-1]
if 'url' in textbook: if 'url' in textbook:
textbook['url'] = remap_static_url(textbook['url'], course) textbook['url'] = remap_static_url(textbook['url'], course)
# then remap all the chapter URLs as well, if they are provided. # then remap all the chapter URLs as well, if they are provided.
...@@ -77,13 +87,17 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -77,13 +87,17 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
for entry in textbook['chapters']: for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course) entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_pdfbook.html', return render_to_response(
{'book_index': book_index, 'static_pdfbook.html',
'course': course, {
'textbook': textbook, 'book_index': book_index,
'chapter': chapter, 'course': course,
'page': page, 'textbook': textbook,
'staff_access': staff_access}) 'chapter': chapter,
'page': page,
'staff_access': staff_access,
},
)
@login_required @login_required
...@@ -109,16 +123,6 @@ def html_index(request, course_id, book_index, chapter=None): ...@@ -109,16 +123,6 @@ def html_index(request, course_id, book_index, chapter=None):
raise Http404("Invalid book index value: {0}".format(book_index)) raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.html_textbooks[book_index] textbook = course.html_textbooks[book_index]
def remap_static_url(original_url, course):
input_url = "'" + original_url + "'"
output_url = replace_static_urls(
input_url,
getattr(course, 'data_dir', None),
course_namespace=course.location
)
# strip off the quotes again...
return output_url[1:-1]
if 'url' in textbook: if 'url' in textbook:
textbook['url'] = remap_static_url(textbook['url'], course) textbook['url'] = remap_static_url(textbook['url'], course)
# then remap all the chapter URLs as well, if they are provided. # then remap all the chapter URLs as well, if they are provided.
...@@ -126,10 +130,14 @@ def html_index(request, course_id, book_index, chapter=None): ...@@ -126,10 +130,14 @@ def html_index(request, course_id, book_index, chapter=None):
for entry in textbook['chapters']: for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course) entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_htmlbook.html', return render_to_response(
{'book_index': book_index, 'static_htmlbook.html',
'course': course, {
'textbook': textbook, 'book_index': book_index,
'chapter': chapter, 'course': course,
'staff_access': staff_access, 'textbook': textbook,
'notes_enabled': notes_enabled}) 'chapter': chapter,
'staff_access': staff_access,
'notes_enabled': notes_enabled,
},
)
...@@ -29,6 +29,9 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True ...@@ -29,6 +29,9 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
# Enabling SQL tracking logs for testing on common/djangoapps/track
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it. # Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True WIKI_ENABLED = True
......
if $('.instructor-dashboard-wrapper').length == 1
analytics.track "Loaded an Instructor Dashboard Page",
location: window.location.pathname
dashboard_page: $('.navbar .selectedmode').text()
...@@ -257,7 +257,6 @@ body.discussion { ...@@ -257,7 +257,6 @@ body.discussion {
font-size: 11px; font-size: 11px;
line-height: 16px; line-height: 16px;
color: #333; color: #333;
outline: 0;
} }
} }
...@@ -932,7 +931,6 @@ body.discussion { ...@@ -932,7 +931,6 @@ body.discussion {
font-size: 11px; font-size: 11px;
line-height: 16px; line-height: 16px;
color: #333; color: #333;
outline: 0;
} }
.post-search { .post-search {
...@@ -959,7 +957,6 @@ body.discussion { ...@@ -959,7 +957,6 @@ body.discussion {
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
color: #333; color: #333;
outline: 0;
cursor: pointer; cursor: pointer;
pointer-events: none; pointer-events: none;
@include transition(all .2s ease-out); @include transition(all .2s ease-out);
...@@ -1642,7 +1639,6 @@ body.discussion { ...@@ -1642,7 +1639,6 @@ body.discussion {
border-radius: 3px; border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset; box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset;
@include transition(border-color .1s); @include transition(border-color .1s);
outline: 0;
&:focus { &:focus {
border-color: #4697c1; border-color: #4697c1;
......
...@@ -84,6 +84,14 @@ a:link, a:visited { ...@@ -84,6 +84,14 @@ a:link, a:visited {
} }
} }
a:focus {
/**
* Add general focus styling here
* for example:
* outline: 3px groove $black;
**/
}
.content-wrapper { .content-wrapper {
width: flex-grid(12); width: flex-grid(12);
margin: 0 auto; margin: 0 auto;
......
...@@ -9,9 +9,6 @@ html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 10 ...@@ -9,9 +9,6 @@ html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 10
html, button, input, select, textarea { font-family: sans-serif; color: #222; } html, button, input, select, textarea { font-family: sans-serif; color: #222; }
body { margin: 0; font-size: 1em; line-height: 1.4; } body { margin: 0; font-size: 1em; line-height: 1.4; }
::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; }
::selection { background: #fe57a1; color: #fff; text-shadow: none; }
a { color: #00e; } a { color: #00e; }
a:visited { color: #551a8b; } a:visited { color: #551a8b; }
a:hover { color: #06e; } a:hover { color: #06e; }
......
...@@ -61,6 +61,8 @@ $baseFontColor: rgb(60,60,60); ...@@ -61,6 +61,8 @@ $baseFontColor: rgb(60,60,60);
$lighter-base-font-color: rgb(100,100,100); $lighter-base-font-color: rgb(100,100,100);
$text-color: $dark-gray; $text-color: $dark-gray;
$dark-trans-bg: rgba(0, 0, 0, .75);
$body-bg: rgb(250,250,250); $body-bg: rgb(250,250,250);
$container-bg: $white; $container-bg: $white;
$header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9)); $header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9));
...@@ -104,8 +106,6 @@ $border-color-4: rgb(252,252,252); ...@@ -104,8 +106,6 @@ $border-color-4: rgb(252,252,252);
$link-color: $blue; $link-color: $blue;
$link-color-d1: $m-blue; $link-color-d1: $m-blue;
$link-hover: $pink; $link-hover: $pink;
$selection-color-1: $pink;
$selection-color-2: #444;
$site-status-color: $pink; $site-status-color: $pink;
$button-color: $blue; $button-color: $blue;
......
...@@ -101,12 +101,6 @@ img { ...@@ -101,12 +101,6 @@ img {
max-width: 100%; max-width: 100%;
} }
::selection, ::-moz-selection, ::-webkit-selection {
background: $selection-color-2;
color: #fff;
}
.tooltip { .tooltip {
position: absolute; position: absolute;
top: 0; top: 0;
......
...@@ -23,7 +23,7 @@ section.course-index { ...@@ -23,7 +23,7 @@ section.course-index {
h3 { h3 {
@include border-radius(0); @include border-radius(0);
margin: 0; margin: 0;
overflow: hidden; overflow: visible;
&:first-child { &:first-child {
border: none; border: none;
......
...@@ -148,7 +148,7 @@ header.global.slim { ...@@ -148,7 +148,7 @@ header.global.slim {
float: left; float: left;
font-size: 0.9em; font-size: 0.9em;
font-weight: 600; font-weight: 600;
color: #777; color: $lighter-base-font-color;
letter-spacing: 0; letter-spacing: 0;
margin-top: 9px; margin-top: 9px;
margin-bottom: 0; margin-bottom: 0;
......
...@@ -268,7 +268,7 @@ ...@@ -268,7 +268,7 @@
@include transition(color 0.15s ease-in-out); @include transition(color 0.15s ease-in-out);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: tint($outer-border-color, 50%); color: $lighter-base-font-color;
font-size: em(13); font-size: em(13);
} }
......
...@@ -561,7 +561,7 @@ ...@@ -561,7 +561,7 @@
float: right; float: right;
display: block; display: block;
font-style: italic; font-style: italic;
color: #a0a0a0; color: $lighter-base-font-color;
text-decoration: underline; text-decoration: underline;
font-size: .8em; font-size: .8em;
margin-top: 32px; margin-top: 32px;
......
...@@ -66,12 +66,17 @@ ...@@ -66,12 +66,17 @@
width: 0; width: 0;
} }
a {
position: relative;
display: block;
}
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
.meta-info { .meta-info {
background: rgba(0,0,0, 0.6); background: $dark-trans-bg;
bottom: 6px; bottom: 6px;
border: 1px solid rgba(0,0,0, 0.5); border: 1px solid rgba(0,0,0, 0.5);
@include border-right-radius(2px); @include border-right-radius(2px);
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
</header> </header>
<section class="info"> <section class="info">
<div class="cover-image"> <div class="cover-image">
<img src="${course_image_url(course)}"> <img src="${course_image_url(course)}" alt="${course.number} ${get_course_about_section(course, 'title')} Cover Image" />
</div> </div>
<div class="desc"> <div class="desc">
<p>${get_course_about_section(course, 'short_description')}</p> <p>${get_course_about_section(course, 'short_description')}</p>
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
<hgroup> <hgroup>
<div class="logo"> <div class="logo">
% if self.stanford_theme_enabled(): % if self.stanford_theme_enabled():
<img src="${static.url('themes/stanford/images/seal.png')}" /> <img src="${static.url('themes/stanford/images/seal.png')}" alt="Stanford Seal Logo" />
% else: % else:
<img src="${static.url('images/edx_bw.png')}" /> <img src="${static.url('images/edx_bw.png')}" alt="Black and White edX Logo" />
% endif % endif
</div> </div>
% if self.stanford_theme_enabled(): % if self.stanford_theme_enabled():
......
...@@ -104,7 +104,7 @@ function goto( mode) ...@@ -104,7 +104,7 @@ function goto( mode)
<section class="instructor-dashboard-content"> <section class="instructor-dashboard-content">
<h1>Instructor Dashboard</h1> <h1>Instructor Dashboard</h1>
<h2>[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> | <h2 class="navbar">[ <a href="#" onclick="goto('Grades');" class="${modeflag.get('Grades')}">Grades</a> |
%if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): %if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'):
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> | <a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
%endif %endif
......
...@@ -211,11 +211,11 @@ ...@@ -211,11 +211,11 @@
% if course.id in show_courseware_links_for: % if course.id in show_courseware_links_for:
<a href="${course_target}" class="cover"> <a href="${course_target}" class="cover">
<img src="${course_image_url(course)}" /> <img src="${course_image_url(course)}" alt="${course.number} ${course.display_name_with_default} Cover Image" />
</a> </a>
% else: % else:
<div class="cover"> <div class="cover">
<img src="${course_image_url(course)}" /> <img src="${course_image_url(course)}" alt="${course.number} ${course.display_name_with_default} Cover Image" />
</div> </div>
% endif % endif
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
} }
} else { } else {
$('.message.submission-error').addClass('is-shown'); $('.message.submission-error').addClass('is-shown').focus();
$('.message.submission-error .message-copy').html(json.value); $('.message.submission-error .message-copy').html(json.value);
} }
}); });
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
<p class="message-copy">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p> <p class="message-copy">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
</div> </div>
<div role="alert" class="status message submission-error"> <div role="alert" class="status message submission-error" tabindex="-1">
<h3 class="message-title">The following errors occured while logging you in: </h3> <h3 class="message-title">The following errors occured while logging you in: </h3>
<ul class="message-copy"> <ul class="message-copy">
<li>Your email or password is incorrect</li> <li>Your email or password is incorrect</li>
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
$('.message.submission-error').removeClass('is-shown'); $('.message.submission-error').removeClass('is-shown');
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
} else { } else {
$('.status.message.submission-error').addClass('is-shown'); $('.status.message.submission-error').addClass('is-shown').focus();
$('.status.message.submission-error .message-copy').html(json.value).stop().css("display", "block"); $('.status.message.submission-error .message-copy').html(json.value).stop().css("display", "block");
$(".field-error").removeClass('field-error'); $(".field-error").removeClass('field-error');
$("[data-field='"+json.field+"']").addClass('field-error') $("[data-field='"+json.field+"']").addClass('field-error')
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
<p class="message-copy">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p> <p class="message-copy">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
</div> </div>
<div role="alert" class="status message submission-error"> <div role="alert" class="status message submission-error" tabindex="-1">
<h3 class="message-title">The following errors occured while processing your registration: </h3> <h3 class="message-title">The following errors occured while processing your registration: </h3>
<ul class="message-copy"> </ul> <ul class="message-copy"> </ul>
</div> </div>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
data-id="${item['id']}" data-id="${item['id']}"
data-element="${idx+1}" data-element="${idx+1}"
href="javascript:void(0);"> href="javascript:void(0);">
<p>${item['title']}</p> <p class="sr">${item['title']}, ${item['type']}</p>
</a> </a>
</li> </li>
% endfor % endfor
......
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
<!-- dummy segment.io --> <!-- dummy segment.io -->
<script type="text/javascript"> <script type="text/javascript">
var analytics = { var analytics = {
track: function() { return; } track: function() { return; },
pageview: function() { return; }
}; };
</script> </script>
<!-- end dummy segment.io --> <!-- end dummy segment.io -->
......
...@@ -214,8 +214,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -214,8 +214,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^mktg/(?P<course_id>.*)$', url(r'^mktg/(?P<course_id>.*)$',
'courseware.views.mktg_course_about', name="mktg_about_course"), 'courseware.views.mktg_course_about', name="mktg_about_course"),
#Inside the course #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
'courseware.views.course_info', name="course_root"), 'courseware.views.course_info', name="course_root"),
...@@ -223,27 +221,26 @@ if settings.COURSEWARE_ENABLED: ...@@ -223,27 +221,26 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.course_info', name="info"), 'courseware.views.course_info', name="info"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/syllabus$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/syllabus$',
'courseware.views.syllabus', name="syllabus"), # TODO arjun remove when custom tabs in place, see courseware/courses.py 'courseware.views.syllabus', name="syllabus"), # TODO arjun remove when custom tabs in place, see courseware/courses.py
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/$',
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>\d+)/$',
'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>\d+)/(?P<page>\d+)$',
'staticbook.views.index', name="book"), 'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
'staticbook.views.index_shifted'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/$',
'staticbook.views.pdf_index', name="pdf_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/(?P<page>\d+)$',
'staticbook.views.pdf_index', name="pdf_book"), 'staticbook.views.pdf_index', name="pdf_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.pdf_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/chapter/(?P<chapter>\d+)/$',
'staticbook.views.pdf_index'), 'staticbook.views.pdf_index', name="pdf_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/chapter/(?P<chapter>\d+)/(?P<page>\d+)$',
'staticbook.views.pdf_index'), 'staticbook.views.pdf_index', name="pdf_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>\d+)/$',
'staticbook.views.html_index', name="html_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>\d+)/chapter/(?P<chapter>\d+)/$',
'staticbook.views.html_index', name="html_book"), 'staticbook.views.html_index', name="html_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
......
...@@ -39,7 +39,8 @@ disable= ...@@ -39,7 +39,8 @@ disable=
# C0301: Line too long # C0301: Line too long
# W0141: Used builtin function 'map' # W0141: Used builtin function 'map'
# W0142: Used * or ** magic # W0142: Used * or ** magic
I0011,C0301,W0141,W0142, # R0922: Abstract class is only referenced 1 times
I0011,C0301,W0141,W0142,R0922,
# Django makes classes that trigger these # Django makes classes that trigger these
# W0232: Class has no __init__ method # W0232: Class has no __init__ method
...@@ -74,7 +75,7 @@ include-ids=yes ...@@ -74,7 +75,7 @@ include-ids=yes
files-output=no files-output=no
# Tells whether to display a full report or only the messages # Tells whether to display a full report or only the messages
reports=yes reports=no
# Python expression which should return a note less than 10 (10 is the highest # Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which # note). You have access to the variables errors warning, statement which
...@@ -117,7 +118,7 @@ generated-members= ...@@ -117,7 +118,7 @@ generated-members=
size, size,
content, content,
status_code, status_code,
# For factory_body factories # For factory_boy factories
create create
...@@ -165,7 +166,7 @@ bad-names=foo,bar,baz,toto,tutu,tata ...@@ -165,7 +166,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do # Regular expression which should only match functions or classes name which do
# not require a docstring # not require a docstring
no-docstring-rgx=(__.*__|test_.*) no-docstring-rgx=__.*__|test_.*|setUp|tearDown
[MISCELLANEOUS] [MISCELLANEOUS]
...@@ -206,7 +207,7 @@ init-import=no ...@@ -206,7 +207,7 @@ init-import=no
# A regular expression matching the beginning of the name of dummy variables # A regular expression matching the beginning of the name of dummy variables
# (i.e. not used). # (i.e. not used).
dummy-variables-rgx=_|dummy dummy-variables-rgx=_|dummy|unused|.*_unused
# List of additional names supposed to be defined in builtins. Remember that # List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible. # you should avoid to define new builtins when possible.
......
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