Merge branch 'felix/hinter2' of https://github.com/edx/edx-platform into felix/hinter2
Conflicts: common/lib/xmodule/xmodule/crowdsource_hinter.py
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -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)
|
||||
|
||||
69
cms/djangoapps/contentstore/features/component.feature
Normal file
69
cms/djangoapps/contentstore/features/component.feature
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
126
cms/djangoapps/contentstore/features/component.py
Normal file
126
cms/djangoapps/contentstore/features/component.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#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'])
|
||||
|
||||
36
cms/djangoapps/contentstore/tests/test_request_event.py
Normal file
36
cms/djangoapps/contentstore/tests/test_request_event.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
|
||||
@@ -3,448 +3,310 @@
|
||||
|
||||
.wrapper-header {
|
||||
@extend .depth3;
|
||||
margin: 0;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $gray;
|
||||
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.2));
|
||||
background: $white;
|
||||
height: 76px;
|
||||
@include box-shadow(0 1px 2px 0 $shadow-l1);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
color: $baseFontColor;
|
||||
display: block;
|
||||
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
background: $white;
|
||||
|
||||
header.primary {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix();
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-l1;
|
||||
}
|
||||
|
||||
nav .nav-item {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// ====================
|
||||
// basic layout
|
||||
|
||||
// basic layout
|
||||
.wrapper-left, .wrapper-right {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
.wrapper-left {
|
||||
width: flex-grid(10, 12);
|
||||
float: left;
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.wrapper-right {
|
||||
width: flex-grid(2, 12);
|
||||
float: right;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - branding
|
||||
.branding, .info-course, .nav-course, .nav-account, .nav-unauth, .nav-pitch {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.branding {
|
||||
position: relative;
|
||||
margin: 0 ($baseline/2) 0 0;
|
||||
padding-right: ($baseline*0.75);
|
||||
|
||||
a {
|
||||
@extend .text-hide;
|
||||
display: block;
|
||||
width: 164px;
|
||||
height: 32px;
|
||||
background: transparent url('../img/logo-edx-studio.png') 0 0 no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - course name/info
|
||||
.info-course {
|
||||
@include font-size(14);
|
||||
position: relative;
|
||||
margin: -3px ($baseline/2) 0 0;
|
||||
padding-right: ($baseline*0.75);
|
||||
|
||||
&:before {
|
||||
@extend .faded-vertical-divider;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: -8px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -12px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.course-number, .course-org {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.course-org {
|
||||
margin-right: ($baseline/4);
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
margin-top: -4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@include font-size(16);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - course nav
|
||||
.nav-course {
|
||||
width: 290px;
|
||||
@extend .t-copy-sub1;
|
||||
margin-top: -($baseline/4);
|
||||
|
||||
> ol > .nav-item {
|
||||
vertical-align: bottom;
|
||||
margin: 0 ($baseline/2) 0 0;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
padding: 0 ($baseline/4);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
|
||||
.label-prefix {
|
||||
@include font-size(11);
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// specific nav items
|
||||
&.nav-course-courseware {
|
||||
}
|
||||
|
||||
&.nav-course-settings {
|
||||
}
|
||||
|
||||
&.nav-course-tools {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - account-based nav
|
||||
.nav-account {
|
||||
width: 100%;
|
||||
margin-top: ($baseline*0.75);
|
||||
@include font-size(14);
|
||||
text-align: right;
|
||||
|
||||
.nav-account-username {
|
||||
width: 100%;
|
||||
|
||||
.icon-user {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
@include font-size(12);
|
||||
}
|
||||
|
||||
.account-username {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon-expand {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// UI - dropdown
|
||||
.nav-dropdown {
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
|
||||
.icon-caret-down {
|
||||
@include font-size(14);
|
||||
@include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.icon-caret-down {
|
||||
color: $blue;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-nav-sub {
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 47px;
|
||||
width: 140px;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
@include border-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1));
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid $gray-l2;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
.wrapper-l, .wrapper-r {
|
||||
background: $white;
|
||||
|
||||
&:after, &:before {
|
||||
bottom: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
border-bottom-color: #fff;
|
||||
border-width: 5px;
|
||||
right: 3px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-color: rgba(178, 178, 178, 0);
|
||||
border-bottom-color: $gray-l2;
|
||||
border-width: 6px;
|
||||
right: 3px;
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
padding: 0 0($baseline/4) 0;
|
||||
@include font-size(13);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI - dropdown - specific navs
|
||||
&.nav-account {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
top: 27px;
|
||||
left: auto;
|
||||
right: -13px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
text-align: left;
|
||||
|
||||
.icon-expand {
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sub:after {
|
||||
left: auto;
|
||||
right: 11px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: auto;
|
||||
right: 10px;
|
||||
}
|
||||
.wrapper-l {
|
||||
float: left;
|
||||
width: flex-grid(7,12);
|
||||
}
|
||||
|
||||
&.nav-course {
|
||||
|
||||
.nav-course-courseware {
|
||||
|
||||
.nav-sub:after {
|
||||
left: 88px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-course-settings {
|
||||
|
||||
.nav-sub:after {
|
||||
left: 88px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-course-tools {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
top: ($baseline*1.5);
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.nav-sub:after {
|
||||
left: 68px;
|
||||
}
|
||||
|
||||
.nav-sub:before {
|
||||
left: 68px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: is-signed in
|
||||
.is-signedin {
|
||||
|
||||
&.course .branding {
|
||||
|
||||
&:before {
|
||||
@extend .faded-vertical-divider;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: -8px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@extend .faded-vertical-divider-light;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -12px;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: not signed in
|
||||
.not-signedin {
|
||||
|
||||
.wrapper-left {
|
||||
width: flex-grid(4, 12);
|
||||
}
|
||||
|
||||
.wrapper-right {
|
||||
width: flex-grid(8, 12);
|
||||
}
|
||||
|
||||
// STATE: not signed in - unauthenticated nav
|
||||
.nav-not-signedin {
|
||||
.wrapper-r {
|
||||
float: right;
|
||||
margin-top: ($baseline/4);
|
||||
width: flex-grid(4,12);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@include font-size(16);
|
||||
.branding, .info-course, .nav-course, .nav-account, .nav-pitch {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nav-account {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// basic layout - nav items
|
||||
nav {
|
||||
|
||||
> ol > .nav-item {
|
||||
@extend .t-action3;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 $baseline 0 0;
|
||||
font-weight: 600;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: -($baseline/4);
|
||||
display: inline-block;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
.nav-item a {
|
||||
color: $gray-d1;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// basic layout - dropdowns
|
||||
.nav-dd {
|
||||
|
||||
.title {
|
||||
@extend .t-action2;
|
||||
@extend .btn-dd-nav-primary;
|
||||
@include transition(all 0.25s ease-in-out 0);
|
||||
|
||||
.label, .icon-caret-down {
|
||||
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
}
|
||||
|
||||
.icon-caret-down {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.icon-caret-down {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sub .nav-item {
|
||||
|
||||
[class^="icon-"] {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - branding
|
||||
.branding {
|
||||
padding: ($baseline*0.75) 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - course name/info
|
||||
.info-course {
|
||||
margin-right: flex-gutter();
|
||||
border-right: 1px solid $gray-l4;
|
||||
padding: ($baseline*0.75) flex-gutter() ($baseline*0.75) 0;
|
||||
|
||||
.course-org, .course-number {
|
||||
@extend .t-action4;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
max-width: 45%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.course-org {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.course-title {
|
||||
@extend .t-action2;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// entire link
|
||||
.course-link {
|
||||
@include transition(color 0.25s ease-in-out);
|
||||
display: block;
|
||||
color: $gray-d1;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - course nav
|
||||
.nav-course {
|
||||
padding: ($baseline*0.75) 0;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - account-based nav
|
||||
.nav-account {
|
||||
position: relative;
|
||||
padding: ($baseline*0.75) 0;
|
||||
|
||||
.nav-sub {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nav-account-help {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
width: ($baseline*10);
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: not signed in - specific items
|
||||
.nav-not-signedin-help {
|
||||
.nav-account-user {
|
||||
|
||||
.title {
|
||||
max-width: ($baseline*6.5);
|
||||
|
||||
> .label {
|
||||
display: inline-block;
|
||||
max-width: 85%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// specific elements - pitch/how it works nav
|
||||
.nav-pitch {
|
||||
position: relative;
|
||||
padding: ($baseline*0.75) 0;
|
||||
|
||||
.nav-sub {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// CASE: user signed in
|
||||
.is-signedin {
|
||||
|
||||
.wrapper-l {
|
||||
width: flex-grid(9,12);
|
||||
}
|
||||
|
||||
.wrapper-r {
|
||||
width: flex-grid(3,12);
|
||||
}
|
||||
|
||||
.branding {
|
||||
width: 20%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.nav-account {
|
||||
top: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// CASE: in course {
|
||||
.is-signedin.course {
|
||||
|
||||
.wrapper-header {
|
||||
|
||||
.wrapper-l {
|
||||
width: flex-grid(9,12);
|
||||
}
|
||||
|
||||
.nav-not-signedin-signup {
|
||||
margin-right: ($baseline/2);
|
||||
.wrapper-r {
|
||||
width: flex-grid(3,12);
|
||||
}
|
||||
|
||||
.branding {
|
||||
width: 20%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.info-course {
|
||||
width: 25%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.nav-course {
|
||||
width: 45%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// CASE: user not signed in
|
||||
.not-signedin {
|
||||
|
||||
.wrapper-header {
|
||||
|
||||
.wrapper-l {
|
||||
width: flex-grid(2,12);
|
||||
}
|
||||
|
||||
.wrapper-r {
|
||||
width: flex-grid(10,12);
|
||||
}
|
||||
|
||||
.branding {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-pitch {
|
||||
top: ($baseline/4);
|
||||
|
||||
.nav-item {
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-signup {
|
||||
@include blue-button;
|
||||
@@ -454,9 +316,6 @@
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-not-signedin-signin {
|
||||
|
||||
.action-signin {
|
||||
@include white-button;
|
||||
@@ -505,12 +364,13 @@ body.course.advanced .nav-course-settings-advanced,
|
||||
// course tools
|
||||
body.course.import .nav-course-tools .title,
|
||||
body.course.export .nav-course-tools .title,
|
||||
body.course.checklists .nav-course-tools .title,
|
||||
|
||||
body.course.import .nav-course-tools-import,
|
||||
body.course.export .nav-course-tools-export,
|
||||
body.course.checklists .nav-course-tools-checklists,
|
||||
|
||||
{
|
||||
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
@@ -518,52 +378,3 @@ body.course.export .nav-course-tools-export,
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
body.signup .nav-not-signedin-signin {
|
||||
|
||||
a {
|
||||
background-color: #d9e3ee;
|
||||
color: #6d788b;
|
||||
}
|
||||
}
|
||||
|
||||
body.signin .nav-not-signedin-signup {
|
||||
|
||||
a {
|
||||
background-color: #62aaf5;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// STATE: js enabled
|
||||
.js {
|
||||
|
||||
.nav-dropdown {
|
||||
|
||||
.nav-item .title {
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:active, &.is-selected {
|
||||
color: $blue;
|
||||
|
||||
.icon-expand {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-nav-sub {
|
||||
@include transition (opacity 1.0s ease-in-out 0s);
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-shown {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,161 @@ nav {
|
||||
|
||||
// ====================
|
||||
|
||||
// primary
|
||||
|
||||
// ====================
|
||||
|
||||
// right hand side
|
||||
|
||||
// ====================
|
||||
|
||||
// tabs
|
||||
|
||||
// ====================
|
||||
|
||||
// dropdown
|
||||
.nav-dd {
|
||||
|
||||
// ====================
|
||||
.title {
|
||||
|
||||
//
|
||||
.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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">> </span>Files & 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">> </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">> </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">> </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>
|
||||
|
||||
@@ -1,64 +1,85 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<div class="wrapper-header wrapper" id="view-top">
|
||||
<header class="primary" role="banner">
|
||||
|
||||
<div class="wrapper wrapper-left ">
|
||||
<h1 class="branding"><a href="/">edX Studio</a></h1>
|
||||
<div class="wrapper wrapper-l">
|
||||
<h1 class="branding"><a href="/"><img src="/static/img/logo-edx-studio.png" alt="edX Studio" /></a></h1>
|
||||
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<div class="info-course">
|
||||
<h2 class="sr">Current Course:</h2>
|
||||
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
|
||||
<span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
|
||||
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
|
||||
<h2 class="info-course">
|
||||
<span class="sr">Current Course:</span>
|
||||
<a class="course-link" href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
|
||||
<span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
|
||||
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
|
||||
</a>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<nav class="nav-course primary nav-dropdown" role="navigation">
|
||||
<nav class="nav-course nav-dd ui-left">
|
||||
<h2 class="sr">${context_course.display_name_with_default}'s Navigation:</h2>
|
||||
|
||||
<ol>
|
||||
<li class="nav-item nav-course-courseware">
|
||||
<h3 class="title"><span class="label-prefix">Course </span>Content <i class="icon-caret-down"></i></h3>
|
||||
<h3 class="title"><span class="label"><span class="label-prefix sr">Course </span>Content</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-courseware-outline"><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Outline</a></li>
|
||||
<li class="nav-item nav-course-courseware-updates"><a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Updates</a></li>
|
||||
<li class="nav-item nav-course-courseware-pages"><a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}">Static Pages</a></li>
|
||||
<li class="nav-item nav-course-courseware-uploads"><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Files & Uploads</a></li>
|
||||
<li class="nav-item nav-course-courseware-outline">
|
||||
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Outline</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-courseware-updates">
|
||||
<a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Updates</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-courseware-pages">
|
||||
<a href="${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}">Static Pages</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-courseware-uploads">
|
||||
<a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Files & Uploads</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-course-settings">
|
||||
<h3 class="title"><span class="label-prefix">Course </span>Settings <i class="icon-caret-down"></i></h3>
|
||||
<h3 class="title"><span class="label"><span class="label-prefix sr">Course </span>Settings</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-settings-schedule"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Schedule & Details</a></li>
|
||||
<li class="nav-item nav-course-settings-grading"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
|
||||
<li class="nav-item nav-course-settings-team"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
<li class="nav-item nav-course-settings-advanced"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
|
||||
<li class="nav-item nav-course-settings-schedule">
|
||||
<a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Schedule & Details</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-settings-grading">
|
||||
<a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-settings-team">
|
||||
<a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-settings-advanced">
|
||||
<a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-course-tools">
|
||||
<h3 class="title">Tools <i class="icon-caret-down"></i></h3>
|
||||
<h3 class="title"><span class="label">Tools</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-tools-checklists"><a href="${reverse('checklists', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Checklists</a></li>
|
||||
<li class="nav-item nav-course-tools-import"><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Import</a></li>
|
||||
<li class="nav-item nav-course-tools-export"><a href="${reverse('export_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Export</a></li>
|
||||
<li class="nav-item nav-course-tools-checklists">
|
||||
<a href="${reverse('checklists', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Checklists</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-tools-import">
|
||||
<a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Import</a>
|
||||
</li>
|
||||
<li class="nav-item nav-course-tools-export">
|
||||
<a href="${reverse('export_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Export</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,34 +89,53 @@
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="wrapper wrapper-right">
|
||||
<div class="wrapper wrapper-r">
|
||||
% if user.is_authenticated():
|
||||
<nav class="nav-account nav-is-signedin nav-dropdown">
|
||||
<h2 class="sr">Currently logged in as:</h2>
|
||||
<nav class="nav-account nav-is-signedin nav-dd ui-right">
|
||||
<h2 class="sr">Help & Account Navigation</h2>
|
||||
|
||||
<ol>
|
||||
<li class="nav-item nav-account-username">
|
||||
<a href="#" class="title">
|
||||
<span class="account-username">
|
||||
<i class="icon-user"></i>
|
||||
${ user.username }
|
||||
</span>
|
||||
<i class="icon-caret-down"></i>
|
||||
</a>
|
||||
<li class="nav-item nav-account-help">
|
||||
<h3 class="title"><span class="label">Help</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-account-dashboard"><a href="/">My Courses</a></li>
|
||||
<li class="nav-item nav-account-help"><a href="http://help.edge.edx.org/" rel="external">Studio Help</a></li>
|
||||
<li class="nav-item nav-account-signout"><a class="action action-signout" href="${reverse('logout')}">Sign Out</a></li>
|
||||
<li class="nav-item nav-help-documentation">
|
||||
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" title="This is a PDF Document">Studio Documentation</a>
|
||||
</li>
|
||||
<li class="nav-item nav-help-helpcenter">
|
||||
<a href="http://help.edge.edx.org/" rel="external">Studio Help Center</a>
|
||||
</li>
|
||||
<li class="nav-item nav-help-feedback">
|
||||
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to share your feedback">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-account-user">
|
||||
<h3 class="title"><span class="label"><span class="label-prefix sr">Currently signed in as:</span><span class="account-username" title="${ user.username }">${ user.username }</span></span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-account-dashboard">
|
||||
<a href="/">My Courses</a>
|
||||
</li>
|
||||
<li class="nav-item nav-account-signout">
|
||||
<a class="action action-signout" href="${reverse('logout')}">Sign Out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
% else:
|
||||
<nav class="nav-not-signedin">
|
||||
<nav class="nav-not-signedin nav-pitch">
|
||||
<h2 class="sr">You're not currently signed in</h2>
|
||||
<ol>
|
||||
<li class="nav-item nav-not-signedin-hiw">
|
||||
|
||||
@@ -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,20 +153,35 @@ 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)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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)
|
||||
|
||||
56
common/djangoapps/track/tests.py
Normal file
56
common/djangoapps/track/tests.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""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)
|
||||
|
||||
|
||||
20
common/djangoapps/util/sandboxing.py
Normal file
20
common/djangoapps/util/sandboxing.py
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
27
common/djangoapps/util/tests/test_sandboxing.py
Normal file
27
common/djangoapps/util/tests/test_sandboxing.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
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) ->
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
"""
|
||||
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 {}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
"""
|
||||
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')
|
||||
|
||||
# The rest of kwargs become attributes on the course:
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(new_course, k, v)
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), own_metadata(new_course))
|
||||
|
||||
data = kwargs.get('data')
|
||||
if data is not None:
|
||||
store.update_item(new_course.location, data)
|
||||
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*:
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 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),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
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",
|
||||
|
||||
231
lms/djangoapps/staticbook/tests.py
Normal file
231
lms/djangoapps/staticbook/tests.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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')
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
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})
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
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 -->
|
||||
|
||||
31
lms/urls.py
31
lms/urls.py
@@ -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"),
|
||||
|
||||
11
pylintrc
11
pylintrc
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user