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>
Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com>
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,
in roughly chronological order, most recent first. Add your entries at or near
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
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"
This is now done when they click on the link in the reset password
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
Instructor Dashboard to allow a particular student's submission for a
particular problem to be rescored. Provides an option to see a
......
......@@ -152,6 +152,12 @@ otherwise noted.
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
-----------------
......
......@@ -115,7 +115,7 @@ def clickActionLink(checklist, task, actionText):
# text will be empty initially, wait for it to populate
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)
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):
@step(u'I should see the date "([^"]*)"$')
def check_date(_step, date):
date_css = 'span.date-display'
date_html = world.css_find(date_css)
assert date == date_html.html
assert date == world.css_html(date_css)
@step(u'I modify the handout to "([^"]*)"$')
......@@ -74,8 +73,7 @@ def edit_handouts(_step, text):
@step(u'I see the handout "([^"]*)"$')
def check_handout(_step, handout):
handout_css = 'div.handouts-content'
handouts = world.css_find(handout_css)
assert handout in handouts.html
assert handout in world.css_html(handout_css)
def change_text(text):
......
......@@ -47,7 +47,7 @@ def confirm_change(step):
range_css = '.range'
all_ranges = world.css_find(range_css)
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 "([^"]*)"$')
......
......@@ -9,14 +9,14 @@ from selenium.webdriver.common.keys import Keys
def go_to_static(_step):
menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages'
world.css_find(menu_css).click()
world.css_find(static_css).click()
world.css_click(menu_css)
world.css_click(static_css)
@step(u'I add a new page')
def add_page(_step):
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$')
......@@ -33,13 +33,13 @@ def click_edit_delete(_step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete
index = get_index(page)
assert index != -1
world.css_find(button_css)[index].click()
world.css_click(button_css, index=index)
@step(u'I change the name to "([^"]*)"$')
def change_name(_step, new_name):
settings_css = '#settings-mode'
world.css_find(settings_css).click()
world.css_click(settings_css)
input_css = 'input.setting-input'
name_input = world.css_find(input_css)
old_name = name_input.value
......@@ -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(new_name)
save_button = 'a.save-button'
world.css_find(save_button).click()
world.css_click(save_button)
def get_index(name):
page_name_css = 'section[data-type="HTMLModule"]'
all_pages = world.css_find(page_name_css)
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 -1
......@@ -16,14 +16,14 @@ HTTP_PREFIX = "http://localhost:8001"
def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware'
uploads_css = 'li.nav-course-courseware-uploads'
world.css_find(menu_css).click()
world.css_find(uploads_css).click()
world.css_click(menu_css)
world.css_click(uploads_css)
@step(u'I upload the file "([^"]*)"$')
def upload_file(_step, file_name):
upload_css = 'a.upload-button'
world.css_find(upload_css).click()
world.css_click(upload_css)
file_css = 'input.file-input'
upload = world.css_find(file_css)
......@@ -32,7 +32,7 @@ def upload_file(_step, file_name):
upload._element.send_keys(os.path.abspath(path))
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$')
......@@ -67,7 +67,7 @@ def no_duplicate(_step, file_name):
all_names = world.css_find(names_css)
only_one = False
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
assert only_one
......@@ -100,7 +100,7 @@ def get_index(file_name):
names_css = 'td.name-col > a.filename'
all_names = world.css_find(names_css)
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 -1
......
......@@ -344,6 +344,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
err_cnt = perform_xlint('common/test/data', ['full'])
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):
direct_store = modulestore('direct')
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
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .access import has_access
from ..utils import get_course_for_item
__all__ = ['preview_dispatch', 'preview_component']
......@@ -93,6 +96,8 @@ def preview_module_system(request, preview_id, descriptor):
MongoUsage(preview_id, descriptor.location.url()),
)
course_id = get_course_for_item(descriptor.location).location.course_id
return ModuleSystem(
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?
......@@ -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),
user=request.user,
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)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
......@@ -140,3 +140,6 @@ SEGMENT_IO_KEY = '***REMOVED***'
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
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() {
// nav - dropdown related
$body.click(function(e) {
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
$('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.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');
$title = $(this).parent().find('.title');
......@@ -71,8 +71,8 @@ $(document).ready(function() {
$subnav.removeClass('is-shown');
$title.removeClass('is-selected');
} else {
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dd .nav-item .title').removeClass('is-selected');
$('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
$title.addClass('is-selected');
$subnav.addClass('is-shown');
}
......
......@@ -47,7 +47,7 @@ $gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
$blue: rgb(85, 151, 221);
$blue: rgb(0, 159, 230);
$blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%);
......
......@@ -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 {
// ====================
// 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 @@
@include box-shadow(0 2px 3px $shadow);
height: ($baseline*35) !important;
background: $white !important;
border: 1px solid $gray;
border: 2px solid $blue;
}
#tender_window {
......@@ -23,11 +23,12 @@
}
#tender_closer {
color: $blue-l2 !important;
color: $white-t2 !important;
text-transform: uppercase;
top: 16px !important;
&:hover {
color: $blue-l4 !important;
color: $white !important;
}
}
......@@ -42,15 +43,15 @@
font-family: 'Open Sans', sans-serif;
}
.widget-layout .search,
.widget-layout .tabs,
.widget-layout .footer,
.widget-layout .search,
.widget-layout .tabs,
.widget-layout .footer,
.widget-layout .header h1 a {
display: none;
}
.widget-layout .header {
background: rgb(85, 151, 221);
background: rgb(0, 159, 230);
padding: 10px 20px;
}
......@@ -264,4 +265,4 @@
.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active {
background-color: #16ca57;
color: #fff;
}
\ No newline at end of file
}
......@@ -72,14 +72,7 @@ body.index {
}
.logo {
@extend .text-hide;
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;
font-weight: 600;
}
.tagline {
......
......@@ -316,6 +316,12 @@ body.course.settings {
.link-courseURL {
@extend .t-copy-lead1;
@include box-sizing(border-box);
display: block;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
......
......@@ -40,7 +40,7 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Files &amp; Uploads
</h1>
......
......@@ -44,7 +44,7 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Course Updates
</h1>
......
......@@ -19,7 +19,7 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Static Pages
</h1>
......
......@@ -121,7 +121,7 @@
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Course Content</small>
<small class="subtitle">Content</small>
<span class="sr">&gt; </span>Course Outline
</h1>
......@@ -165,9 +165,9 @@
<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>
%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>
<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>
%endif
</div>
......
......@@ -23,7 +23,6 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase):
@staticmethod
def topic_name_to_id(course, name):
"""
......@@ -34,7 +33,6 @@ class TestCohorts(django.test.TestCase):
run=course.url_name,
name=name)
@staticmethod
def config_course_cohorts(course, discussions,
cohorted,
......@@ -80,7 +78,6 @@ class TestCohorts(django.test.TestCase):
course.cohort_config = d
def setUp(self):
"""
Make sure that course is reloaded every time--clear out the modulestore.
......@@ -89,7 +86,6 @@ class TestCohorts(django.test.TestCase):
# to course. We don't have a course.clone() method.
_MODULESTORES.clear()
def test_get_cohort(self):
"""
Make sure get_cohort() does the right thing when the course is cohorted
......@@ -105,7 +101,7 @@ class TestCohorts(django.test.TestCase):
cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course.id,
group_type=CourseUserGroup.COHORT)
group_type=CourseUserGroup.COHORT)
cohort.users.add(user)
......@@ -135,7 +131,7 @@ class TestCohorts(django.test.TestCase):
cohort = CourseUserGroup.objects.create(name="TestCohort",
course_id=course.id,
group_type=CourseUserGroup.COHORT)
group_type=CourseUserGroup.COHORT)
# user1 manually added to a cohort
cohort.users.add(user1)
......@@ -169,7 +165,6 @@ class TestCohorts(django.test.TestCase):
self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
"user2 should still be in originally placed cohort")
def test_auto_cohorting_randomization(self):
"""
Make sure get_cohort() randomizes properly.
......@@ -199,8 +194,6 @@ class TestCohorts(django.test.TestCase):
self.assertGreater(num_users, 1)
self.assertLess(num_users, 50)
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
course2_id = 'e/f/g'
......@@ -214,14 +207,12 @@ class TestCohorts(django.test.TestCase):
course_id=course1_id,
group_type=CourseUserGroup.COHORT)
# second course should have no cohorts
self.assertEqual(get_course_cohorts(course2_id), [])
cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
def test_is_commentable_cohorted(self):
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertFalse(course.is_cohorted)
......
......@@ -153,21 +153,36 @@ def click_link(partial_text):
@world.absorb
def css_text(css_selector):
def css_text(css_selector, index=0):
# Wait for the css selector to appear
if world.is_css_present(css_selector):
try:
return world.browser.find_by_css(css_selector).first.text
return world.browser.find_by_css(css_selector)[index].text
except StaleElementReferenceException:
# The DOM was still redrawing. Wait a second and try again.
world.wait(1)
return world.browser.find_by_css(css_selector).first.text
return world.browser.find_by_css(css_selector)[index].text
else:
return ""
@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):
assert is_css_present(css_selector)
return world.browser.find_by_css(css_selector).visible
......
from django.db import models
from django.db import models
class TrackingLog(models.Model):
"""Defines the fields that are stored in the tracking log database"""
dtcreated = models.DateTimeField('creation date', auto_now_add=True)
username = models.CharField(max_length=32, blank=True)
ip = models.CharField(max_length=32, blank=True)
......@@ -16,6 +15,9 @@ class TrackingLog(models.Model):
host = models.CharField(max_length=64, blank=True)
def __unicode__(self):
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source,
self.event_type, self.page, self.event)
return s
fmt = (
u"[{self.time}] {self.username}@{self.ip}: "
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):
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
username = request.user.username
......@@ -59,13 +60,14 @@ def user_track(request):
"session": scookie,
"ip": request.META['REMOTE_ADDR'],
"event_source": "browser",
"event_type": request.GET['event_type'],
"event": request.GET['event'],
"event_type": request.REQUEST['event_type'],
"event": request.REQUEST['event'],
"agent": agent,
"page": request.GET['page'],
"page": request.REQUEST['page'],
"time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'],
}
}
log_event(event)
return HttpResponse('success')
......@@ -92,7 +94,7 @@ def server_track(request, event_type, event, page=None):
"page": page,
"time": datetime.datetime.now(UTC).isoformat(),
"host": request.META['SERVER_NAME'],
}
}
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
return
......@@ -136,7 +138,7 @@ def task_track(request_info, task_info, event_type, event, page=None):
"page": page,
"time": datetime.datetime.utcnow().isoformat(),
"host": request_info.get('host', 'unknown')
}
}
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):
"""
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.
"""
if dispatch == 'get_hint':
out = self.get_hint(get)
out = self.get_hint(data)
elif dispatch == 'get_feedback':
out = self.get_feedback(get)
out = self.get_feedback(data)
elif dispatch == 'vote':
out = self.tally_vote(get)
out = self.tally_vote(data)
elif dispatch == 'submit_hint':
out = self.submit_hint(get)
out = self.submit_hint(data)
else:
return json.dumps({'contents': 'Error - invalid operation.'})
......@@ -128,16 +128,16 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
out.update({'op': dispatch})
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.
Args:
`get` -- must be interpretable by capa_answer_to_str.
`data` -- must be interpretable by capa_answer_to_str.
Output keys:
- '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.
"""
try:
......@@ -181,12 +181,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
'rand_hint_2': rand_hint_2,
'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.
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:
- '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.
......@@ -221,20 +221,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
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.
Args:
`get` -- expected to have the following keys:
`data` -- expected to have the following keys:
'answer': ans_no (index in previous_answers)
'hint': hint_pk
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
"""
if self.user_voted:
return {}
ans_no = int(get['answer'])
hint_no = str(get['hint'])
ans_no = int(data['answer'])
hint_no = str(data['hint'])
answer = self.previous_answers[ans_no][0]
# We use temp_dict because we need to do a direct write for the database to update.
temp_dict = self.hints
......@@ -254,19 +254,19 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
self.previous_answers = []
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.
Args:
`get` -- expected to have the following keys:
`data` -- expected to have the following keys:
'answer': answer index in previous_answers
'hint': text of the new hint that the user is adding
Returns a thank-you message.
"""
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint = escape(get['hint'])
answer = self.previous_answers[int(get['answer'])][0]
hint = escape(data['hint'])
answer = self.previous_answers[int(data['answer'])][0]
# Only allow a student to vote or submit a hint once.
if self.user_voted:
return {'message': 'Sorry, but you have already voted!'}
......
......@@ -16,12 +16,12 @@
#answer-tabs .ui-widget-header {
border-bottom: 1px solid #DCDCDC;
background: #F3F3F3;
background: #FDF8EB;
}
#answer-tabs .ui-tabs-nav .ui-state-default {
border: 1px solid #DCDCDC;
background: #F8F8F8;
background: #E6E6E3;
margin-bottom: 0px;
}
......
......@@ -166,7 +166,6 @@ nav.sequence-nav {
p {
background: #333;
color: #fff;
display: none;
font-family: $sans-serif;
line-height: lh();
left: 0px;
......
......@@ -111,7 +111,15 @@ class @Sequence
if (1 <= new_position) and (new_position <= @num_contents)
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
if window.queuePollerID
window.clearTimeout(window.queuePollerID)
......@@ -125,12 +133,30 @@ class @Sequence
event.preventDefault()
new_position = @position + 1
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
previous: (event) =>
event.preventDefault()
new_position = @position - 1
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
link_for: (position) ->
......
"""
Provide names as exported by older mongo.py module
"""
from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore, MongoUsage
# 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 sys
import logging
......@@ -19,8 +33,7 @@ from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
from xblock.core import Scope
from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son
from xmodule.modulestore.exceptions import (ItemNotFoundError,
DuplicateItemError)
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateItemError
from xmodule.modulestore.inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata
log = logging.getLogger(__name__)
......@@ -32,6 +45,7 @@ log = logging.getLogger(__name__)
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])
......@@ -615,6 +629,9 @@ class MongoModuleStore(ModuleStoreBase):
return item
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:
self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id,
location=location)
......@@ -758,5 +775,3 @@ class MongoModuleStore(ModuleStoreBase):
are loaded on demand, rather than up front
"""
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 xmodule.modulestore import Location, namedtuple_to_son
......@@ -217,7 +225,6 @@ class DraftModuleStore(MongoModuleStore):
def _query_children_for_cache_children(self, items):
# 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_dict = {}
......@@ -243,7 +250,6 @@ class DraftModuleStore(MongoModuleStore):
to_process_dict[draft_as_non_draft_loc] = draft
# convert the dict - which is used for look ups - back into a list
for key, value in to_process_dict.iteritems():
queried_children.append(value)
queried_children = to_process_dict.values()
return queried_children
......@@ -19,14 +19,13 @@ class XModuleCourseFactory(Factory):
ABSTRACT_FACTORY = True
@classmethod
def _create(cls, target_class, *args, **kwargs):
def _create(cls, target_class, **kwargs):
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.get('org')
number = kwargs.get('number')
display_name = kwargs.get('display_name')
location = Location('i4x', org, number,
'course', Location.clean(display_name))
org = kwargs.pop('org', None)
number = kwargs.pop('number', None)
display_name = kwargs.pop('display_name', None)
location = Location('i4x', org, number, 'course', Location.clean(display_name))
try:
store = modulestore('direct')
......@@ -41,7 +40,7 @@ class XModuleCourseFactory(Factory):
new_course.display_name = display_name
new_course.lms.start = datetime.datetime.now(UTC)
new_course.tabs = kwargs.get(
new_course.tabs = kwargs.pop(
'tabs',
[
{"type": "courseware"},
......@@ -51,14 +50,14 @@ class XModuleCourseFactory(Factory):
{"type": "progress", "name": "Progress"}
]
)
new_course.discussion_link = kwargs.get('discussion_link')
# Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course))
# The rest of kwargs become attributes on the course:
for k, v in kwargs.iteritems():
setattr(new_course, k, v)
data = kwargs.get('data')
if data is not None:
store.update_item(new_course.location, data)
# Update the data in the mongo datastore
store.update_metadata(new_course.location, own_metadata(new_course))
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 the instance we are working with, so have to refetch the course after updating it.
......@@ -101,7 +100,7 @@ class XModuleItemFactory(Factory):
return parent._replace(category=attr.category, name=dest_name)
@classmethod
def _create(cls, target_class, *args, **kwargs):
def _create(cls, target_class, **kwargs):
"""
Uses *kwargs*:
......
"""
Methods for exporting course data to XML
"""
import logging
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from fs.osfs import OSFS
from json import dumps
import json
from json.encoder import JSONEncoder
import datetime
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):
if isinstance(obj, Location):
return obj.url()
......@@ -22,7 +32,19 @@ class EdxJSONEncoder(json.JSONEncoder):
else:
return super(EdxJSONEncoder, self).default(obj)
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)
......
# Tests for xmodule.util.date_utils
"""Tests for xmodule.util.date_utils"""
from nose.tools import assert_equals, assert_false
from xmodule.util.date_utils import get_default_time_display, almost_same_datetime
......@@ -19,6 +19,7 @@ def test_get_default_time_display():
"Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False))
def test_get_default_time_display_notz():
test_time = datetime(1992, 3, 12, 15, 3, 30)
assert_equals(
......@@ -31,8 +32,10 @@ def test_get_default_time_display_notz():
"Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False))
# pylint: disable=W0232
class NamelessTZ(tzinfo):
"""Static timezone for testing"""
def utcoffset(self, _dt):
return timedelta(hours=-3)
......@@ -40,6 +43,7 @@ class NamelessTZ(tzinfo):
def dst(self, _dt):
return timedelta(0)
def test_get_default_time_display_no_tzname():
assert_equals("", get_default_time_display(None))
test_time = datetime(1992, 3, 12, 15, 3, 30, tzinfo=NamelessTZ())
......@@ -53,6 +57,7 @@ def test_get_default_time_display_no_tzname():
"Mar 12, 1992 at 15:03",
get_default_time_display(test_time, False))
def test_almost_same_datetime():
assert almost_same_datetime(
datetime(2013, 5, 3, 10, 20, 30),
......
"""
Tests of XML export
"""
import unittest
import pytz
from datetime import datetime, timedelta, tzinfo
from fs.osfs import OSFS
from mock import Mock
from path import path
from tempfile import mkdtemp
import shutil
......@@ -136,19 +139,22 @@ class RoundTripTestCase(unittest.TestCase):
class TestEdxJsonEncoder(unittest.TestCase):
"""
Tests for xml_exporter.EdxJSONEncoder
"""
def setUp(self):
self.encoder = EdxJSONEncoder()
class OffsetTZ(tzinfo):
"""A timezone with non-None utcoffset"""
def utcoffset(self, dt):
def utcoffset(self, _dt):
return timedelta(hours=4)
self.offset_tz = OffsetTZ()
class NullTZ(tzinfo):
"""A timezone with None as its utcoffset"""
def utcoffset(self, dt):
def utcoffset(self, _dt):
return None
self.null_utc_tz = NullTZ()
......
"""
Convenience methods for working with datetime objects
"""
import datetime
def get_default_time_display(dt, show_timezone=True):
"""
......
......@@ -3,20 +3,10 @@ describe 'Logger', ->
expect(window.log_event).toBe Logger.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', ->
spyOn $, 'getWithPrefix'
spyOn $, 'postWithPrefix'
Logger.log 'example', 'data'
expect($.getWithPrefix).toHaveBeenCalledWith '/event',
expect($.postWithPrefix).toHaveBeenCalledWith '/event',
event_type: 'example'
event: '"data"'
page: window.location.href
......
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 = {}
@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.
if event_type of listeners
# Cool. Do the elements also match?
......@@ -28,7 +17,7 @@ class @Logger
callback(event_type, data, element)
# Regardless of whether any callbacks were made, log this event.
$.getWithPrefix '/event',
$.postWithPrefix '/event',
event_type: event_type
event: JSON.stringify(data)
page: window.location.href
......@@ -43,7 +32,6 @@ class @Logger
else
listeners[event_type][element].push callback
@bind: ->
window.onunload = ->
$.ajaxWithPrefix
......@@ -54,5 +42,5 @@ class @Logger
page: window.location.href
async: false
# Keeping this for conpatibility issue only.
# Keeping this for compatibility issue only.
@log_event = Logger.log
......@@ -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,
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`.
- 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
from courseware.masquerade import setup_masquerade
from courseware.model_data import LmsKeyValueStore, LmsUsage, ModelDataCache
from courseware.models import StudentModule
from util.sandboxing import can_execute_unsafe_code
log = logging.getLogger(__name__)
......@@ -61,9 +61,9 @@ def make_track_function(request):
'''
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 f
return function
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):
should go back to the LMS, not to the worker.
"""
prefix = '{proto}://{host}'.format(
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'),
host=request.get_host()
)
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'),
host=request.get_host()
)
return settings.XQUEUE_INTERFACE.get('callback_url', prefix)
......@@ -313,14 +313,6 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
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
# prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from
......@@ -348,7 +340,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
open_ended_grading_interface=open_ended_grading_interface,
s3_interface=s3_interface,
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
system.set('position', position)
......
......@@ -32,13 +32,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
1. TEMPLATE_NAME
2. DATA
3. MODEL_DATA
4. COURSE_DATA and USER_COUNT if needed
This class should not contain any tests, because TEMPLATE_NAME
should be defined in child class.
"""
USER_COUNT = 2
COURSE_DATA = {}
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
TEMPLATE_NAME = ""
......@@ -47,7 +45,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create(data=self.COURSE_DATA)
self.course = CourseFactory.create()
# Turn off cache.
modulestore().request_cache = None
......
......@@ -3,7 +3,6 @@ import datetime
from django.test import TestCase
from django.http import Http404
from django.conf import settings
from django.test.utils import override_settings
from django.contrib.auth.models import User
from django.test.client import RequestFactory
......@@ -52,8 +51,8 @@ class ViewsTestCase(TestCase):
self.date = datetime.datetime(2013, 1, 22, tzinfo=UTC)
self.course_id = 'edX/toy/2012_Fall'
self.enrollment = CourseEnrollment.objects.get_or_create(user=self.user,
course_id=self.course_id,
created=self.date)[0]
course_id=self.course_id,
created=self.date)[0]
self.location = ['tag', 'org', 'course', 'category', 'name']
self._MODULESTORES = {}
# This is a CourseDescriptor object
......
......@@ -6,10 +6,8 @@ Unit tests for enrollment methods in views.py
from django.test.utils import override_settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......
......@@ -27,11 +27,11 @@ class TestGradebook(ModuleStoreTestCase):
modulestore().request_cache = modulestore().metadata_inheritance_cache_subsystem = None
course_data = {}
kwargs = {}
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(
parent_location=self.course.location,
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.http import Http404
from mitxmako.shortcuts import render_to_response
......@@ -10,6 +14,9 @@ from static_replace import replace_static_urls
@login_required
def index(request, course_id, book_index, page=None):
"""
Serve static image-based textbooks.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
......@@ -22,18 +29,31 @@ def index(request, course_id, book_index, page=None):
if page is None:
page = textbook.start_page
return render_to_response('staticbook.html',
{'book_index': book_index, 'page': int(page),
'course': course,
'book_url': textbook.book_url,
'table_of_contents': table_of_contents,
'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)
return render_to_response(
'staticbook.html',
{
'book_index': book_index, 'page': int(page),
'course': course,
'book_url': textbook.book_url,
'table_of_contents': table_of_contents,
'start_page': textbook.start_page,
'end_page': textbook.end_page,
'staff_access': staff_access,
},
)
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
......@@ -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))
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:
textbook['url'] = remap_static_url(textbook['url'], course)
# 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):
for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_pdfbook.html',
{'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'page': page,
'staff_access': staff_access})
return render_to_response(
'static_pdfbook.html',
{
'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'page': page,
'staff_access': staff_access,
},
)
@login_required
......@@ -109,16 +123,6 @@ def html_index(request, course_id, book_index, chapter=None):
raise Http404("Invalid book index value: {0}".format(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:
textbook['url'] = remap_static_url(textbook['url'], course)
# 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):
for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_htmlbook.html',
{'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'staff_access': staff_access,
'notes_enabled': notes_enabled})
return render_to_response(
'static_htmlbook.html',
{
'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'staff_access': staff_access,
'notes_enabled': notes_enabled,
},
)
......@@ -29,6 +29,9 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = 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.
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 {
font-size: 11px;
line-height: 16px;
color: #333;
outline: 0;
}
}
......@@ -932,7 +931,6 @@ body.discussion {
font-size: 11px;
line-height: 16px;
color: #333;
outline: 0;
}
.post-search {
......@@ -959,7 +957,6 @@ body.discussion {
font-size: 13px;
line-height: 20px;
color: #333;
outline: 0;
cursor: pointer;
pointer-events: none;
@include transition(all .2s ease-out);
......@@ -1642,7 +1639,6 @@ body.discussion {
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset;
@include transition(border-color .1s);
outline: 0;
&:focus {
border-color: #4697c1;
......
......@@ -84,6 +84,14 @@ a:link, a:visited {
}
}
a:focus {
/**
* Add general focus styling here
* for example:
* outline: 3px groove $black;
**/
}
.content-wrapper {
width: flex-grid(12);
margin: 0 auto;
......
......@@ -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; }
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:visited { color: #551a8b; }
a:hover { color: #06e; }
......
......@@ -61,6 +61,8 @@ $baseFontColor: rgb(60,60,60);
$lighter-base-font-color: rgb(100,100,100);
$text-color: $dark-gray;
$dark-trans-bg: rgba(0, 0, 0, .75);
$body-bg: rgb(250,250,250);
$container-bg: $white;
$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);
$link-color: $blue;
$link-color-d1: $m-blue;
$link-hover: $pink;
$selection-color-1: $pink;
$selection-color-2: #444;
$site-status-color: $pink;
$button-color: $blue;
......
......@@ -101,12 +101,6 @@ img {
max-width: 100%;
}
::selection, ::-moz-selection, ::-webkit-selection {
background: $selection-color-2;
color: #fff;
}
.tooltip {
position: absolute;
top: 0;
......
......@@ -23,7 +23,7 @@ section.course-index {
h3 {
@include border-radius(0);
margin: 0;
overflow: hidden;
overflow: visible;
&:first-child {
border: none;
......
......@@ -148,7 +148,7 @@ header.global.slim {
float: left;
font-size: 0.9em;
font-weight: 600;
color: #777;
color: $lighter-base-font-color;
letter-spacing: 0;
margin-top: 9px;
margin-bottom: 0;
......
......@@ -268,7 +268,7 @@
@include transition(color 0.15s ease-in-out);
display: block;
margin-top: ($baseline/4);
color: tint($outer-border-color, 50%);
color: $lighter-base-font-color;
font-size: em(13);
}
......
......@@ -561,7 +561,7 @@
float: right;
display: block;
font-style: italic;
color: #a0a0a0;
color: $lighter-base-font-color;
text-decoration: underline;
font-size: .8em;
margin-top: 32px;
......
......@@ -66,12 +66,17 @@
width: 0;
}
a {
position: relative;
display: block;
}
a:hover {
text-decoration: none;
}
.meta-info {
background: rgba(0,0,0, 0.6);
background: $dark-trans-bg;
bottom: 6px;
border: 1px solid rgba(0,0,0, 0.5);
@include border-right-radius(2px);
......
......@@ -19,7 +19,7 @@
</header>
<section class="info">
<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 class="desc">
<p>${get_course_about_section(course, 'short_description')}</p>
......
......@@ -10,9 +10,9 @@
<hgroup>
<div class="logo">
% 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:
<img src="${static.url('images/edx_bw.png')}" />
<img src="${static.url('images/edx_bw.png')}" alt="Black and White edX Logo" />
% endif
</div>
% if self.stanford_theme_enabled():
......
......@@ -104,7 +104,7 @@ function goto( mode)
<section class="instructor-dashboard-content">
<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'):
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
%endif
......
......@@ -211,11 +211,11 @@
% if course.id in show_courseware_links_for:
<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>
% else:
<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>
% endif
......
......@@ -53,7 +53,7 @@
location.href="${reverse('dashboard')}";
}
} else {
$('.message.submission-error').addClass('is-shown');
$('.message.submission-error').addClass('is-shown').focus();
$('.message.submission-error .message-copy').html(json.value);
}
});
......@@ -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>
</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>
<ul class="message-copy">
<li>Your email or password is incorrect</li>
......
......@@ -54,7 +54,7 @@
$('.message.submission-error').removeClass('is-shown');
location.href="${reverse('dashboard')}";
} 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");
$(".field-error").removeClass('field-error');
$("[data-field='"+json.field+"']").addClass('field-error')
......@@ -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>
</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>
<ul class="message-copy"> </ul>
</div>
......
......@@ -16,7 +16,7 @@
data-id="${item['id']}"
data-element="${idx+1}"
href="javascript:void(0);">
<p>${item['title']}</p>
<p class="sr">${item['title']}, ${item['type']}</p>
</a>
</li>
% endfor
......
......@@ -17,7 +17,8 @@
<!-- dummy segment.io -->
<script type="text/javascript">
var analytics = {
track: function() { return; }
track: function() { return; },
pageview: function() { return; }
};
</script>
<!-- end dummy segment.io -->
......
......@@ -214,8 +214,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^mktg/(?P<course_id>.*)$',
'courseware.views.mktg_course_about', name="mktg_about_course"),
#Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
'courseware.views.course_info', name="course_root"),
......@@ -223,27 +221,26 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.course_info', name="info"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/syllabus$',
'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"),
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"),
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>[^/]*)/$',
'staticbook.views.pdf_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.pdf_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/chapter/(?P<chapter>\d+)/$',
'staticbook.views.pdf_index', name="pdf_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>\d+)/chapter/(?P<chapter>\d+)/(?P<page>\d+)$',
'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"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"),
......
......@@ -39,7 +39,8 @@ disable=
# C0301: Line too long
# W0141: Used builtin function 'map'
# 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
# W0232: Class has no __init__ method
......@@ -74,7 +75,7 @@ include-ids=yes
files-output=no
# 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
# note). You have access to the variables errors warning, statement which
......@@ -117,7 +118,7 @@ generated-members=
size,
content,
status_code,
# For factory_body factories
# For factory_boy factories
create
......@@ -165,7 +166,7 @@ bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=(__.*__|test_.*)
no-docstring-rgx=__.*__|test_.*|setUp|tearDown
[MISCELLANEOUS]
......@@ -206,7 +207,7 @@ init-import=no
# A regular expression matching the beginning of the name of dummy variables
# (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
# 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