Merge branch 'master' into feature/christina/metadata-ui
Conflicts: cms/templates/base.html
This commit is contained in:
@@ -19,9 +19,7 @@ DISPLAY_NAME_VALUE = '"Robot Super Course"'
|
||||
############### ACTIONS ####################
|
||||
@step('I select the Advanced Settings$')
|
||||
def i_select_advanced_settings(step):
|
||||
expand_icon_css = 'li.nav-course-settings i.icon-expand'
|
||||
if world.browser.is_element_present_by_css(expand_icon_css):
|
||||
world.css_click(expand_icon_css)
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-advanced a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ from selenium.common.exceptions import StaleElementReferenceException
|
||||
############### ACTIONS ####################
|
||||
@step('I select Checklists from the Tools menu$')
|
||||
def i_select_checklists(step):
|
||||
expand_icon_css = 'li.nav-course-tools i.icon-expand'
|
||||
if world.browser.is_element_present_by_css(expand_icon_css):
|
||||
world.css_click(expand_icon_css)
|
||||
world.click_tools()
|
||||
link_css = 'li.nav-course-tools-checklists a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ DEFAULT_TIME = "00:00"
|
||||
############### ACTIONS ####################
|
||||
@step('I select Schedule and Details$')
|
||||
def test_i_select_schedule_and_details(step):
|
||||
expand_icon_css = 'li.nav-course-settings i.icon-expand'
|
||||
if world.browser.is_element_present_by_css(expand_icon_css):
|
||||
world.css_click(expand_icon_css)
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-schedule a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@@ -62,4 +62,4 @@ def i_am_on_tab(step, tab_name):
|
||||
@step('I see a link for adding a new section$')
|
||||
def i_see_new_section_link(step):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
assert world.css_has_text(link_css, '+ New Section')
|
||||
assert world.css_has_text(link_css, 'New Section')
|
||||
|
||||
@@ -62,7 +62,7 @@ def i_click_to_edit_section_name(step):
|
||||
|
||||
@step('I see the complete section name with a quote in the editor$')
|
||||
def i_see_complete_section_name_with_quote_in_editor(step):
|
||||
css = '.edit-section-name'
|
||||
css = '.section-name-edit input[type=text]'
|
||||
assert world.is_css_present(css)
|
||||
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
@@ -45,7 +47,7 @@ class MongoCollectionFindWrapper(object):
|
||||
self.counter = 0
|
||||
|
||||
def find(self, query, *args, **kwargs):
|
||||
self.counter = self.counter+1
|
||||
self.counter = self.counter + 1
|
||||
return self.original(query, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -352,7 +354,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
for descriptor in items:
|
||||
new_loc = descriptor.location._replace(org='MITx', course='999')
|
||||
new_loc = descriptor.location.replace(org='MITx', course='999')
|
||||
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -375,15 +377,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
fs = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(fs.exists(dirname))
|
||||
filesystem = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(filesystem.exists(dirname))
|
||||
|
||||
query_loc = Location('i4x', location.org, location.course, category_name, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
for item in items:
|
||||
fs = OSFS(root_dir / ('test_export/' + dirname))
|
||||
self.assertTrue(fs.exists(item.location.name + filename_suffix))
|
||||
filesystem = OSFS(root_dir / ('test_export/' + dirname))
|
||||
self.assertTrue(filesystem.exists(item.location.name + filename_suffix))
|
||||
|
||||
def test_export_course(self):
|
||||
module_store = modulestore('direct')
|
||||
@@ -415,7 +417,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# add private to list of children
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
private_location_no_draft = private_vertical.location._replace(revision=None)
|
||||
private_location_no_draft = private_vertical.location.replace(revision=None)
|
||||
module_store.update_children(sequential.location, sequential.children +
|
||||
[private_location_no_draft.url()])
|
||||
|
||||
@@ -440,20 +442,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
|
||||
# check for graiding_policy.json
|
||||
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
self.assertTrue(fs.exists('grading_policy.json'))
|
||||
filesystem = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
self.assertTrue(filesystem.exists('grading_policy.json'))
|
||||
|
||||
course = module_store.get_item(location)
|
||||
# compare what's on disk compared to what we have in our course
|
||||
with fs.open('grading_policy.json', 'r') as grading_policy:
|
||||
with filesystem.open('grading_policy.json', 'r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
self.assertEqual(on_disk, course.grading_policy)
|
||||
|
||||
#check for policy.json
|
||||
self.assertTrue(fs.exists('policy.json'))
|
||||
self.assertTrue(filesystem.exists('policy.json'))
|
||||
|
||||
# compare what's on disk to what we have in the course module
|
||||
with fs.open('policy.json', 'r') as course_policy:
|
||||
with filesystem.open('policy.json', 'r') as course_policy:
|
||||
on_disk = loads(course_policy.read())
|
||||
self.assertIn('course/6.002_Spring_2012', on_disk)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
|
||||
@@ -608,6 +610,14 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
def test_create_course_check_forum_seeding(self):
|
||||
"""Test new course creation and verify forum seeding """
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course'))
|
||||
|
||||
def test_create_course_duplicate_course(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
@@ -801,37 +811,37 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc._replace(category='sequential', name='test_sequence')
|
||||
subsection_location = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.get(reverse('edit_subsection',
|
||||
kwargs={'location': subsection_location.url()}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# go look at the Edit page
|
||||
unit_location = loc._replace(category='vertical', name='test_vertical')
|
||||
unit_location = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.get(reverse('edit_unit',
|
||||
kwargs={'location': unit_location.url()}))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a component
|
||||
del_loc = loc._replace(category='html', name='test_html')
|
||||
del_loc = loc.replace(category='html', name='test_html')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc._replace(category='vertical', name='test_vertical')
|
||||
del_loc = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc._replace(category='sequential', name='test_sequence')
|
||||
del_loc = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# delete a chapter
|
||||
del_loc = loc._replace(category='chapter', name='chapter_2')
|
||||
del_loc = loc.replace(category='chapter', name='chapter_2')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
@@ -13,17 +13,13 @@ from django.core.urlresolvers import reverse
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions \
|
||||
import ItemNotFoundError, InvalidLocationError
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from contentstore.course_info_model \
|
||||
import get_course_updates, update_course_updates, delete_course_update
|
||||
from contentstore.utils \
|
||||
import get_lms_link_for_item, add_extra_panel_tab, \
|
||||
remove_extra_panel_tab
|
||||
from models.settings.course_details \
|
||||
import CourseDetails, CourseSettingsEncoder
|
||||
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
|
||||
from contentstore.utils import get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab
|
||||
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from auth.authz import create_all_course_groups
|
||||
@@ -35,6 +31,10 @@ from .tabs import initialize_course_tabs
|
||||
from .component import OPEN_ENDED_COMPONENT_TYPES, \
|
||||
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
|
||||
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
|
||||
# TODO: should explicitly enumerate exports with __all__
|
||||
|
||||
__all__ = ['course_index', 'create_new_course', 'course_info',
|
||||
'course_info_updates', 'get_course_settings',
|
||||
'course_config_graders_page',
|
||||
@@ -136,6 +136,9 @@ def create_new_course(request):
|
||||
|
||||
create_all_course_groups(request.user, new_course.location)
|
||||
|
||||
# seed the forums
|
||||
seed_permissions_roles(new_course.location.course_id)
|
||||
|
||||
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
||||
|
||||
|
||||
|
||||
@@ -323,6 +323,9 @@ INSTALLED_APPS = (
|
||||
'pipeline',
|
||||
'staticfiles',
|
||||
'static_replace',
|
||||
|
||||
# comment common
|
||||
'django_comment_common',
|
||||
)
|
||||
|
||||
################# EDX MARKETING SITE ##################################
|
||||
|
||||
@@ -127,8 +127,7 @@ CELERY_ALWAYS_EAGER = True
|
||||
|
||||
################################ DEBUG TOOLBAR #################################
|
||||
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
|
||||
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
||||
DEBUG_TOOLBAR_PANELS = (
|
||||
|
||||
@@ -36,8 +36,13 @@ PIPELINE_JS['spec'] = {
|
||||
}
|
||||
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/cms/jasmine')
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
|
||||
TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
|
||||
|
||||
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
|
||||
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
|
||||
|
||||
# Remove the localization middleware class because it requires the test database
|
||||
# to be sync'd and migrated in order to run the jasmine tests interactively
|
||||
@@ -45,4 +50,4 @@ STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
|
||||
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
|
||||
if e != 'django.middleware.locale.LocaleMiddleware')
|
||||
|
||||
INSTALLED_APPS += ('django_jasmine', )
|
||||
INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')
|
||||
|
||||
@@ -64,10 +64,6 @@
|
||||
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/metadata_editor_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
</script>
|
||||
|
||||
<script src="${static.url('js/models/feedback.js')}"></script>
|
||||
<script src="${static.url('js/views/feedback.js')}"></script>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/checklists_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/checklists.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
|
||||
@@ -15,7 +15,6 @@ from contentstore import utils
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
|
||||
|
||||
@@ -11,7 +11,6 @@ from contentstore import utils
|
||||
<%block name="jsextra">
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script>
|
||||
|
||||
@@ -6,27 +6,26 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<!-- -->
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Settings</h1>
|
||||
<article class="settings-overview">
|
||||
<div class="settings-page-section main-column">
|
||||
@@ -74,7 +73,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-1-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="remove-item remove-faculty-data"><span class="delete-icon"></span> Delete Faculty Member</a>
|
||||
@@ -102,7 +101,7 @@ from contentstore import utils
|
||||
<a href="#" class="new-item new-faculty-photo add-faculty-photo-data" id="course-faculty-2-photo">
|
||||
<span class="upload-icon"></span>Upload Faculty Photo
|
||||
</a>
|
||||
<span class="tip tip-inline">Max size: 30KB</span>
|
||||
<span class="tip tip-inline">Max size: 30KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +113,7 @@ from contentstore import utils
|
||||
<textarea class="long tall edit-box tinymce" id="course-faculty-2-bio"></textarea>
|
||||
<span class="tip tip-stacked">A brief description of your education, experience, and expertise</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -143,7 +142,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-general-randomization" id="course-problems-general-randomization-always" value="Always">
|
||||
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-general-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
@@ -217,7 +216,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-problems-assignment-1-randomization" id="course-problems-assignment-1-randomization-always" value="Always">
|
||||
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-problems-assignment-1-randomization-always">Always</label>
|
||||
<span class="tip tip-stacked"><strong>randomize all</strong> problems</span>
|
||||
@@ -283,7 +282,7 @@ from contentstore import utils
|
||||
|
||||
<section class="settings-discussions">
|
||||
<h2 class="title">Discussions</h2>
|
||||
|
||||
|
||||
<section class="settings-discussions-general">
|
||||
<header>
|
||||
<h3>General Settings</h3>
|
||||
@@ -296,7 +295,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
@@ -320,7 +319,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input input-radio">
|
||||
<input checked="checked" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-allow" value="Allow">
|
||||
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-allow">Allow</label>
|
||||
<span class="tip tip-stacked">Students and faculty <strong>will be able to post anonymously</strong></span>
|
||||
@@ -329,7 +328,7 @@ from contentstore import utils
|
||||
|
||||
<div class="input input-radio">
|
||||
<input disabled="disabled" type="radio" name="course-discussions-anonymous" id="course-discussions-anonymous-dontallow" value="Do Not Allow">
|
||||
|
||||
|
||||
<div class="copy">
|
||||
<label for="course-discussions-anonymous-dontallow">Do not allow</label>
|
||||
<span class="tip tip-stacked">This option is disabled since there are previous discussions that are anonymous.</span>
|
||||
@@ -351,7 +350,7 @@ from contentstore import utils
|
||||
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="input input-existing input-default course-discussions-categories-list-item sortable-item">
|
||||
<div class="group">
|
||||
<label for="course-discussions-categories-2-name">Category Name: </label>
|
||||
|
||||
@@ -12,7 +12,6 @@ from contentstore import utils
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
|
||||
0
common/djangoapps/django_comment_common/__init__.py
Normal file
0
common/djangoapps/django_comment_common/__init__.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.v2 import SchemaMigration
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
#
|
||||
# cdodge: This is basically an empty migration since everything has - up to now - managed in the django_comment_client app
|
||||
# But going forward we should be using this migration
|
||||
#
|
||||
def forwards(self, orm):
|
||||
pass
|
||||
|
||||
def backwards(self, orm):
|
||||
pass
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'django_comment_common.permission': {
|
||||
'Meta': {'object_name': 'Permission'},
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}),
|
||||
'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_common.Role']"})
|
||||
},
|
||||
'django_comment_common.role': {
|
||||
'Meta': {'object_name': 'Role'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['django_comment_common']
|
||||
74
common/djangoapps/django_comment_common/models.py
Normal file
74
common/djangoapps/django_comment_common/models.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
FORUM_ROLE_ADMINISTRATOR = 'Administrator'
|
||||
FORUM_ROLE_MODERATOR = 'Moderator'
|
||||
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
|
||||
FORUM_ROLE_STUDENT = 'Student'
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False)
|
||||
users = models.ManyToManyField(User, related_name="roles")
|
||||
course_id = models.CharField(max_length=255, blank=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
# use existing table that was originally created from django_comment_client app
|
||||
db_table = 'django_comment_client_role'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name + " for " + (self.course_id if self.course_id else "all courses")
|
||||
|
||||
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
|
||||
# since it's one-off and doesn't handle inheritance later
|
||||
if role.course_id and role.course_id != self.course_id:
|
||||
logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency", \
|
||||
self, role)
|
||||
for per in role.permissions.all():
|
||||
self.add_permission(per)
|
||||
|
||||
def add_permission(self, permission):
|
||||
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
|
||||
|
||||
def has_permission(self, permission):
|
||||
course_loc = CourseDescriptor.id_to_location(self.course_id)
|
||||
course = modulestore().get_instance(self.course_id, course_loc)
|
||||
if self.name == FORUM_ROLE_STUDENT and \
|
||||
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
|
||||
(not course.forum_posts_allowed):
|
||||
return False
|
||||
|
||||
return self.permissions.filter(name=permission).exists()
|
||||
|
||||
|
||||
class Permission(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
|
||||
roles = models.ManyToManyField(Role, related_name="permissions")
|
||||
|
||||
class Meta:
|
||||
# use existing table that was originally created from django_comment_client app
|
||||
db_table = 'django_comment_client_permission'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
56
common/djangoapps/django_comment_common/utils.py
Normal file
56
common/djangoapps/django_comment_common/utils.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django_comment_common.models import Role
|
||||
|
||||
_STUDENT_ROLE_PERMISSIONS = ["vote", "update_thread", "follow_thread", "unfollow_thread",
|
||||
"update_comment", "create_sub_comment", "unvote", "create_thread",
|
||||
"follow_commentable", "unfollow_commentable", "create_comment", ]
|
||||
|
||||
_MODERATOR_ROLE_PERMISSIONS = ["edit_content", "delete_thread", "openclose_thread",
|
||||
"endorse_comment", "delete_comment", "see_all_cohorts"]
|
||||
|
||||
_ADMINISTRATOR_ROLE_PERMISSIONS = ["manage_moderator"]
|
||||
|
||||
def seed_permissions_roles(course_id):
|
||||
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
|
||||
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
|
||||
community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
|
||||
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
|
||||
|
||||
for per in _STUDENT_ROLE_PERMISSIONS:
|
||||
student_role.add_permission(per)
|
||||
|
||||
for per in _MODERATOR_ROLE_PERMISSIONS:
|
||||
moderator_role.add_permission(per)
|
||||
|
||||
for per in _ADMINISTRATOR_ROLE_PERMISSIONS:
|
||||
administrator_role.add_permission(per)
|
||||
|
||||
moderator_role.inherit_permissions(student_role)
|
||||
|
||||
# For now, Community TA == Moderator, except for the styling.
|
||||
community_ta_role.inherit_permissions(moderator_role)
|
||||
|
||||
administrator_role.inherit_permissions(moderator_role)
|
||||
|
||||
|
||||
def are_permissions_roles_seeded(course_id):
|
||||
|
||||
try:
|
||||
administrator_role = Role.objects.get(name="Administrator", course_id=course_id)
|
||||
moderator_role = Role.objects.get(name="Moderator", course_id=course_id)
|
||||
student_role = Role.objects.get(name="Student", course_id=course_id)
|
||||
except:
|
||||
return False
|
||||
|
||||
for per in _STUDENT_ROLE_PERMISSIONS:
|
||||
if not student_role.has_permission(per):
|
||||
return False
|
||||
|
||||
for per in _MODERATOR_ROLE_PERMISSIONS + _STUDENT_ROLE_PERMISSIONS:
|
||||
if not moderator_role.has_permission(per):
|
||||
return False
|
||||
|
||||
for per in _ADMINISTRATOR_ROLE_PERMISSIONS + _MODERATOR_ROLE_PERMISSIONS + _STUDENT_ROLE_PERMISSIONS:
|
||||
if not administrator_role.has_permission(per):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,43 +1,47 @@
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
CourseEnrollmentAllowed, CourseEnrollment,
|
||||
PendingEmailChange)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
|
||||
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
|
||||
from uuid import uuid4
|
||||
|
||||
# Factories don't have __init__ methods, and are self documenting
|
||||
# pylint: disable=W0232
|
||||
|
||||
|
||||
class GroupFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'staff_MITx/999/Robot_Super_Course'
|
||||
name = u'staff_MITx/999/Robot_Super_Course'
|
||||
|
||||
|
||||
class UserProfileFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
name = u'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
gender = u'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
goals = u'World domination'
|
||||
|
||||
|
||||
class RegistrationFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
activation_key = uuid4().hex.decode('ascii')
|
||||
|
||||
|
||||
class UserFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
username = Sequence(u'robot{0}'.format)
|
||||
email = Sequence(u'robot+test+{0}@edx.org'.format)
|
||||
password = PostGenerationMethodCall('set_password',
|
||||
'test')
|
||||
first_name = 'Robot'
|
||||
first_name = Sequence(u'Robot{0}'.format)
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
@@ -64,7 +68,7 @@ class CourseEnrollmentFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = CourseEnrollment
|
||||
|
||||
user = SubFactory(UserFactory)
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
course_id = u'edX/toy/2012_Fall'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(DjangoModelFactory):
|
||||
@@ -72,3 +76,17 @@ class CourseEnrollmentAllowedFactory(DjangoModelFactory):
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
|
||||
|
||||
class PendingEmailChangeFactory(DjangoModelFactory):
|
||||
"""Factory for PendingEmailChange objects
|
||||
|
||||
user: generated by UserFactory
|
||||
new_email: sequence of new+email+{}@edx.org
|
||||
activation_key: sequence of integers, padded to 30 characters
|
||||
"""
|
||||
FACTORY_FOR = PendingEmailChange
|
||||
|
||||
user = SubFactory(UserFactory)
|
||||
new_email = Sequence(u'new+email+{0}@edx.org'.format)
|
||||
activation_key = Sequence(u'{:0<30d}'.format)
|
||||
|
||||
261
common/djangoapps/student/tests/test_email.py
Normal file
261
common/djangoapps/student/tests/test_email.py
Normal file
@@ -0,0 +1,261 @@
|
||||
import json
|
||||
import django.db
|
||||
|
||||
from student.tests.factories import UserFactory, RegistrationFactory, PendingEmailChangeFactory
|
||||
from student.views import reactivation_email_for_user, change_email_request, confirm_email_change
|
||||
from student.models import UserProfile, PendingEmailChange
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from mock import Mock, patch
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.conf import settings
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
"""Exception used for testing that nothing will catch explicitly"""
|
||||
pass
|
||||
|
||||
|
||||
def mock_render_to_string(template_name, context):
|
||||
"""Return a string that encodes template_name and context"""
|
||||
return str((template_name, sorted(context.iteritems())))
|
||||
|
||||
|
||||
def mock_render_to_response(template_name, context):
|
||||
"""Return an HttpResponse with content that encodes template_name and context"""
|
||||
return HttpResponse(mock_render_to_string(template_name, context))
|
||||
|
||||
|
||||
class EmailTestMixin(object):
|
||||
"""Adds useful assertions for testing `email_user`"""
|
||||
|
||||
def assertEmailUser(self, email_user, subject_template, subject_context, body_template, body_context):
|
||||
"""Assert that `email_user` was used to send and email with the supplied subject and body
|
||||
|
||||
`email_user`: The mock `django.contrib.auth.models.User.email_user` function
|
||||
to verify
|
||||
`subject_template`: The template to have been used for the subject
|
||||
`subject_context`: The context to have been used for the subject
|
||||
`body_template`: The template to have been used for the body
|
||||
`body_context`: The context to have been used for the body
|
||||
"""
|
||||
email_user.assert_called_with(
|
||||
mock_render_to_string(subject_template, subject_context),
|
||||
mock_render_to_string(body_template, body_context),
|
||||
settings.DEFAULT_FROM_EMAIL
|
||||
)
|
||||
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
@patch('django.contrib.auth.models.User.email_user')
|
||||
class ReactivationEmailTests(EmailTestMixin, TestCase):
|
||||
"""Test sending a reactivation email to a user"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.registration = RegistrationFactory.create(user=self.user)
|
||||
|
||||
def reactivation_email(self):
|
||||
"""Send the reactivation email, and return the response as json data"""
|
||||
return json.loads(reactivation_email_for_user(self.user).content)
|
||||
|
||||
def assertReactivateEmailSent(self, email_user):
|
||||
"""Assert that the correct reactivation email has been sent"""
|
||||
context = {
|
||||
'name': self.user.profile.name,
|
||||
'key': self.registration.activation_key
|
||||
}
|
||||
|
||||
self.assertEmailUser(
|
||||
email_user,
|
||||
'emails/activation_email_subject.txt',
|
||||
context,
|
||||
'emails/activation_email.txt',
|
||||
context
|
||||
)
|
||||
|
||||
def test_reactivation_email_failure(self, email_user):
|
||||
self.user.email_user.side_effect = Exception
|
||||
response_data = self.reactivation_email()
|
||||
|
||||
self.assertReactivateEmailSent(email_user)
|
||||
self.assertFalse(response_data['success'])
|
||||
|
||||
def test_reactivation_email_success(self, email_user):
|
||||
response_data = self.reactivation_email()
|
||||
|
||||
self.assertReactivateEmailSent(email_user)
|
||||
self.assertTrue(response_data['success'])
|
||||
|
||||
|
||||
class EmailChangeRequestTests(TestCase):
|
||||
"""Test changing a user's email address"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.new_email = 'new.email@edx.org'
|
||||
self.req_factory = RequestFactory()
|
||||
self.request = self.req_factory.post('unused_url', data={
|
||||
'password': 'test',
|
||||
'new_email': self.new_email
|
||||
})
|
||||
self.request.user = self.user
|
||||
self.user.email_user = Mock()
|
||||
|
||||
def run_request(self, request=None):
|
||||
"""Execute request and return result parsed as json
|
||||
|
||||
If request isn't passed in, use self.request instead
|
||||
"""
|
||||
if request is None:
|
||||
request = self.request
|
||||
|
||||
response = change_email_request(self.request)
|
||||
return json.loads(response.content)
|
||||
|
||||
def assertFailedRequest(self, response_data, expected_error):
|
||||
"""Assert that `response_data` indicates a failed request that returns `expected_error`"""
|
||||
self.assertFalse(response_data['success'])
|
||||
self.assertEquals(expected_error, response_data['error'])
|
||||
self.assertFalse(self.user.email_user.called)
|
||||
|
||||
def test_unauthenticated(self):
|
||||
self.user.is_authenticated = False
|
||||
with self.assertRaises(Http404):
|
||||
change_email_request(self.request)
|
||||
self.assertFalse(self.user.email_user.called)
|
||||
|
||||
def test_invalid_password(self):
|
||||
self.request.POST['password'] = 'wrong'
|
||||
self.assertFailedRequest(self.run_request(), 'Invalid password')
|
||||
|
||||
def test_invalid_emails(self):
|
||||
for email in ('bad_email', 'bad_email@', '@bad_email'):
|
||||
self.request.POST['new_email'] = email
|
||||
self.assertFailedRequest(self.run_request(), 'Valid e-mail address required.')
|
||||
|
||||
def check_duplicate_email(self, email):
|
||||
"""Test that a request to change a users email to `email` fails"""
|
||||
request = self.req_factory.post('unused_url', data={
|
||||
'new_email': email,
|
||||
'password': 'test',
|
||||
})
|
||||
request.user = self.user
|
||||
self.assertFailedRequest(self.run_request(request), 'An account with this e-mail already exists.')
|
||||
|
||||
def test_duplicate_email(self):
|
||||
UserFactory.create(email=self.new_email)
|
||||
self.check_duplicate_email(self.new_email)
|
||||
|
||||
def test_capitalized_duplicate_email(self):
|
||||
raise SkipTest("We currently don't check for emails in a case insensitive way, but we should")
|
||||
UserFactory.create(email=self.new_email)
|
||||
self.check_duplicate_email(self.new_email.capitalize())
|
||||
|
||||
# TODO: Finish testing the rest of change_email_request
|
||||
|
||||
|
||||
@patch('django.contrib.auth.models.User.email_user')
|
||||
@patch('student.views.render_to_response', Mock(side_effect=mock_render_to_response, autospec=True))
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
class EmailChangeConfirmationTests(EmailTestMixin, TransactionTestCase):
|
||||
"""Test that confirmation of email change requests function even in the face of exceptions thrown while sending email"""
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.profile = UserProfile.objects.get(user=self.user)
|
||||
self.req_factory = RequestFactory()
|
||||
self.request = self.req_factory.get('unused_url')
|
||||
self.request.user = self.user
|
||||
self.user.email_user = Mock()
|
||||
self.pending_change_request = PendingEmailChangeFactory.create(user=self.user)
|
||||
self.key = self.pending_change_request.activation_key
|
||||
|
||||
def assertRolledBack(self):
|
||||
"""Assert that no changes to user, profile, or pending email have been made to the db"""
|
||||
self.assertEquals(self.user.email, User.objects.get(username=self.user.username).email)
|
||||
self.assertEquals(self.profile.meta, UserProfile.objects.get(user=self.user).meta)
|
||||
self.assertEquals(1, PendingEmailChange.objects.count())
|
||||
|
||||
def assertFailedBeforeEmailing(self, email_user):
|
||||
"""Assert that the function failed before emailing a user"""
|
||||
self.assertRolledBack()
|
||||
self.assertFalse(email_user.called)
|
||||
|
||||
def check_confirm_email_change(self, expected_template, expected_context):
|
||||
"""Call `confirm_email_change` and assert that the content was generated as expected
|
||||
|
||||
`expected_template`: The name of the template that should have been used
|
||||
to generate the content
|
||||
`expected_context`: The context dictionary that should have been used to
|
||||
generate the content
|
||||
"""
|
||||
response = confirm_email_change(self.request, self.key)
|
||||
self.assertEquals(
|
||||
mock_render_to_response(expected_template, expected_context).content,
|
||||
response.content
|
||||
)
|
||||
|
||||
def assertChangeEmailSent(self, email_user):
|
||||
"""Assert that the correct email was sent to confirm an email change"""
|
||||
context = {
|
||||
'old_email': self.user.email,
|
||||
'new_email': self.pending_change_request.new_email,
|
||||
}
|
||||
self.assertEmailUser(
|
||||
email_user,
|
||||
'emails/email_change_subject.txt',
|
||||
context,
|
||||
'emails/confirm_email_change.txt',
|
||||
context
|
||||
)
|
||||
|
||||
def test_not_pending(self, email_user):
|
||||
self.key = 'not_a_key'
|
||||
self.check_confirm_email_change('invalid_email_key.html', {})
|
||||
self.assertFailedBeforeEmailing(email_user)
|
||||
|
||||
def test_duplicate_email(self, email_user):
|
||||
UserFactory.create(email=self.pending_change_request.new_email)
|
||||
self.check_confirm_email_change('email_exists.html', {})
|
||||
self.assertFailedBeforeEmailing(email_user)
|
||||
|
||||
def test_old_email_fails(self, email_user):
|
||||
email_user.side_effect = [Exception, None]
|
||||
self.check_confirm_email_change('email_change_failed.html', {
|
||||
'email': self.user.email,
|
||||
})
|
||||
self.assertRolledBack()
|
||||
self.assertChangeEmailSent(email_user)
|
||||
|
||||
def test_new_email_fails(self, email_user):
|
||||
email_user.side_effect = [None, Exception]
|
||||
self.check_confirm_email_change('email_change_failed.html', {
|
||||
'email': self.pending_change_request.new_email
|
||||
})
|
||||
self.assertRolledBack()
|
||||
self.assertChangeEmailSent(email_user)
|
||||
|
||||
def test_successful_email_change(self, email_user):
|
||||
self.check_confirm_email_change('email_change_successful.html', {
|
||||
'old_email': self.user.email,
|
||||
'new_email': self.pending_change_request.new_email
|
||||
})
|
||||
self.assertChangeEmailSent(email_user)
|
||||
meta = json.loads(UserProfile.objects.get(user=self.user).meta)
|
||||
self.assertIn('old_emails', meta)
|
||||
self.assertEquals(self.user.email, meta['old_emails'][0][0])
|
||||
self.assertEquals(
|
||||
self.pending_change_request.new_email,
|
||||
User.objects.get(username=self.user.username).email
|
||||
)
|
||||
self.assertEquals(0, PendingEmailChange.objects.count())
|
||||
|
||||
@patch('student.views.PendingEmailChange.objects.get', Mock(side_effect=TestException))
|
||||
@patch('student.views.transaction.rollback', wraps=django.db.transaction.rollback)
|
||||
def test_always_rollback(self, rollback, _email_user):
|
||||
with self.assertRaises(TestException):
|
||||
confirm_email_change(self.request, self.key)
|
||||
|
||||
rollback.assert_called_with()
|
||||
@@ -19,7 +19,7 @@ from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import validate_email, validate_slug, ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404
|
||||
from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
@@ -655,7 +655,7 @@ def create_account(request, post_override=None):
|
||||
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
except:
|
||||
log.exception(sys.exc_info())
|
||||
log.warning('Unable to send activation email to user', exc_info=True)
|
||||
js['value'] = 'Could not send activation e-mail.'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
@@ -975,7 +975,11 @@ def reactivation_email_for_user(user):
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/activation_email.txt', d)
|
||||
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
try:
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
except:
|
||||
log.warning('Unable to send reactivation email', exc_info=True)
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Unable to send reactivation email'}))
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
@@ -1001,7 +1005,7 @@ def change_email_request(request):
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Valid e-mail address required.'}))
|
||||
|
||||
if len(User.objects.filter(email=new_email)) != 0:
|
||||
if User.objects.filter(email=new_email).count() != 0:
|
||||
## CRITICAL TODO: Handle case sensitivity for e-mails
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'An account with this e-mail already exists.'}))
|
||||
@@ -1036,41 +1040,63 @@ def change_email_request(request):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@transaction.commit_manually
|
||||
def confirm_email_change(request, key):
|
||||
''' User requested a new e-mail. This is called when the activation
|
||||
link is clicked. We confirm with the old e-mail, and update
|
||||
'''
|
||||
try:
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
except PendingEmailChange.DoesNotExist:
|
||||
return render_to_response("invalid_email_key.html", {})
|
||||
try:
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
except PendingEmailChange.DoesNotExist:
|
||||
transaction.rollback()
|
||||
return render_to_response("invalid_email_key.html", {})
|
||||
|
||||
user = pec.user
|
||||
d = {'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
user = pec.user
|
||||
address_context = {
|
||||
'old_email': user.email,
|
||||
'new_email': pec.new_email
|
||||
}
|
||||
|
||||
if len(User.objects.filter(email=pec.new_email)) != 0:
|
||||
return render_to_response("email_exists.html", d)
|
||||
if len(User.objects.filter(email=pec.new_email)) != 0:
|
||||
transaction.rollback()
|
||||
return render_to_response("email_exists.html", {})
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/confirm_email_change.txt', d)
|
||||
up = UserProfile.objects.get(user=user)
|
||||
meta = up.get_meta()
|
||||
if 'old_emails' not in meta:
|
||||
meta['old_emails'] = []
|
||||
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
|
||||
up.set_meta(meta)
|
||||
up.save()
|
||||
# Send it to the old email...
|
||||
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
user.email = pec.new_email
|
||||
user.save()
|
||||
pec.delete()
|
||||
# And send it to the new email...
|
||||
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
subject = render_to_string('emails/email_change_subject.txt', address_context)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/confirm_email_change.txt', address_context)
|
||||
up = UserProfile.objects.get(user=user)
|
||||
meta = up.get_meta()
|
||||
if 'old_emails' not in meta:
|
||||
meta['old_emails'] = []
|
||||
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
|
||||
up.set_meta(meta)
|
||||
up.save()
|
||||
# Send it to the old email...
|
||||
try:
|
||||
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
except Exception:
|
||||
transaction.rollback()
|
||||
log.warning('Unable to send confirmation email to old address', exc_info=True)
|
||||
return render_to_response("email_change_failed.html", {'email': user.email})
|
||||
|
||||
return render_to_response("email_change_successful.html", d)
|
||||
user.email = pec.new_email
|
||||
user.save()
|
||||
pec.delete()
|
||||
# And send it to the new email...
|
||||
try:
|
||||
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
except Exception:
|
||||
transaction.rollback()
|
||||
log.warning('Unable to send confirmation email to new address', exc_info=True)
|
||||
return render_to_response("email_change_failed.html", {'email': pec.new_email})
|
||||
|
||||
transaction.commit()
|
||||
return render_to_response("email_change_successful.html", address_context)
|
||||
except Exception:
|
||||
# If we get an unexpected exception, be sure to rollback the transaction
|
||||
transaction.rollback()
|
||||
raise
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -123,3 +123,17 @@ def save_the_html(path='/tmp'):
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(html)
|
||||
f.close()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_course_settings():
|
||||
course_settings_css = 'li.nav-course-settings'
|
||||
if world.browser.is_element_present_by_css(course_settings_css):
|
||||
world.css_click(course_settings_css)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def click_tools():
|
||||
tools_css = 'li.nav-course-tools'
|
||||
if world.browser.is_element_present_by_css(tools_css):
|
||||
world.css_click(tools_css)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Jasmine Test Runner</title>
|
||||
<link rel="stylesheet" type="text/css" href="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.css">
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.js"></script>
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
|
||||
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.cookie.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/CodeMirror/codemirror.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/RequireJS.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, function() {
|
||||
return "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- SOURCE FILES -->
|
||||
<% for src in js_source %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
|
||||
<!-- SPEC FILES -->
|
||||
<% for src in js_specs %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="text/javascript">
|
||||
var console_reporter = new jasmine.ConsoleReporter()
|
||||
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
|
||||
jasmine.getEnv().addReporter(console_reporter);
|
||||
jasmine.getEnv().execute();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
from collections import namedtuple
|
||||
|
||||
from .exceptions import InvalidLocationError, InsufficientSpecificationError
|
||||
from xmodule.errortracker import ErrorLog, make_error_tracker
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from bson.son import SON
|
||||
|
||||
log = logging.getLogger('mitx.' + 'modulestore')
|
||||
@@ -64,7 +64,6 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return re.sub('_+', '_', invalid.sub('_', value))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def clean(value):
|
||||
"""
|
||||
@@ -72,7 +71,6 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return Location._clean(value, INVALID_CHARS)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def clean_keeping_underscores(value):
|
||||
"""
|
||||
@@ -82,7 +80,6 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return INVALID_CHARS.sub('_', value)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def clean_for_url_name(value):
|
||||
"""
|
||||
@@ -154,9 +151,7 @@ class Location(_LocationBase):
|
||||
to mean wildcard selection.
|
||||
"""
|
||||
|
||||
|
||||
if (org is None and course is None and category is None and
|
||||
name is None and revision is None):
|
||||
if (org is None and course is None and category is None and name is None and revision is None):
|
||||
location = loc_or_tag
|
||||
else:
|
||||
location = (loc_or_tag, org, course, category, name, revision)
|
||||
@@ -191,7 +186,7 @@ class Location(_LocationBase):
|
||||
match = MISSING_SLASH_URL_RE.match(location)
|
||||
if match is None:
|
||||
log.debug('location is instance of %s but no URL match' % basestring)
|
||||
raise InvalidLocationError(location)
|
||||
raise InvalidLocationError(location)
|
||||
groups = match.groupdict()
|
||||
check_dict(groups)
|
||||
return _LocationBase.__new__(_cls, **groups)
|
||||
@@ -233,7 +228,7 @@ class Location(_LocationBase):
|
||||
html id attributes
|
||||
"""
|
||||
s = "-".join(str(v) for v in self.list()
|
||||
if v is not None)
|
||||
if v is not None)
|
||||
return Location.clean_for_html(s)
|
||||
|
||||
def dict(self):
|
||||
@@ -258,6 +253,12 @@ class Location(_LocationBase):
|
||||
at the location URL hierachy"""
|
||||
return "/".join([self.org, self.course, self.name])
|
||||
|
||||
def replace(self, **kwargs):
|
||||
'''
|
||||
Expose a public method for replacing location elements
|
||||
'''
|
||||
return self._replace(**kwargs)
|
||||
|
||||
|
||||
class ModuleStore(object):
|
||||
"""
|
||||
@@ -382,12 +383,6 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_course(self, course_id):
|
||||
'''
|
||||
Look for a specific course id. Returns the course descriptor, or None if not found.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
@@ -406,8 +401,7 @@ class ModuleStore(object):
|
||||
courses = [
|
||||
course
|
||||
for course in self.get_courses()
|
||||
if course.location.org == location.org
|
||||
and course.location.course == location.course
|
||||
if course.location.org == location.org and course.location.course == location.course
|
||||
]
|
||||
|
||||
return courses
|
||||
|
||||
@@ -13,11 +13,12 @@ def as_draft(location):
|
||||
"""
|
||||
return Location(location)._replace(revision=DRAFT)
|
||||
|
||||
|
||||
def as_published(location):
|
||||
"""
|
||||
Returns the Location that is the published version for `location`
|
||||
"""
|
||||
return Location(location)._replace(revision=None)
|
||||
return Location(location)._replace(revision=None)
|
||||
|
||||
|
||||
def wrap_draft(item):
|
||||
|
||||
@@ -3,7 +3,6 @@ from time import gmtime
|
||||
from uuid import uuid4
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.timeparse import stringify_time
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import random
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from xblock.core import Scope, Integer
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -136,6 +136,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'hide_progress_tab': bool_map,
|
||||
'allow_anonymous': bool_map,
|
||||
'allow_anonymous_to_peers': bool_map,
|
||||
'show_timezone': bool_map,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script src="{% static 'jasmine-latest/jasmine-html.js' %}"></script>
|
||||
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
|
||||
<script src="{% static 'console-runner.js' %}"></script>
|
||||
<script src="{% static 'jasmine.junit_reporter.js' %}"></script>
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
@@ -37,15 +38,14 @@
|
||||
|
||||
<script>
|
||||
{% block jasmine %}
|
||||
var console_reporter = new jasmine.ConsoleReporter();
|
||||
(function() {
|
||||
var jasmineEnv = jasmine.getEnv();
|
||||
jasmineEnv.updateInterval = 1000;
|
||||
|
||||
var trivialReporter = new jasmine.TrivialReporter();
|
||||
|
||||
var trivialReporter = new jasmine.TrivialReporter()
|
||||
jasmineEnv.addReporter(trivialReporter);
|
||||
jasmine.getEnv().addReporter(console_reporter);
|
||||
jasmineEnv.addReporter(new jasmine.ConsoleReporter());
|
||||
jasmineEnv.addReporter(new jasmine.JUnitXmlReporter('{{ JASMINE_REPORT_DIR }}/'));
|
||||
|
||||
jasmineEnv.specFilter = function(spec) {
|
||||
return trivialReporter.specFilter(spec);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
|
||||
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
|
||||
<script type="text/javascript" src="<%= jasmine_reporters_path %>/src/jasmine.junit_reporter.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
|
||||
@@ -44,30 +45,10 @@
|
||||
<body>
|
||||
|
||||
<script type="text/javascript">
|
||||
var jasmineEnv = jasmine.getEnv();
|
||||
|
||||
var htmlReporter = new jasmine.HtmlReporter();
|
||||
var console_reporter = new jasmine.ConsoleReporter()
|
||||
|
||||
jasmineEnv.addReporter(htmlReporter);
|
||||
jasmineEnv.addReporter(console_reporter);
|
||||
|
||||
jasmineEnv.specFilter = function(spec) {
|
||||
return htmlReporter.specFilter(spec);
|
||||
};
|
||||
|
||||
var currentWindowOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (currentWindowOnload) {
|
||||
currentWindowOnload();
|
||||
}
|
||||
execJasmine();
|
||||
};
|
||||
|
||||
function execJasmine() {
|
||||
jasmineEnv.execute();
|
||||
}
|
||||
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
|
||||
jasmine.getEnv().addReporter(new jasmine.ConsoleReporter());
|
||||
jasmine.getEnv().addReporter(new jasmine.JUnitXmlReporter('<%= report_dir %>/'));
|
||||
jasmine.getEnv().execute();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" />
|
||||
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" show_timezone="true"/>
|
||||
|
||||
@@ -12,4 +12,13 @@
|
||||
<html slug="html_95">Minor correction: Six elements (five resistors)…</html>
|
||||
<customtag tag="S1" slug="discuss_96" impl="discuss"/>
|
||||
</vertical>
|
||||
|
||||
<randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule">
|
||||
<vertical>
|
||||
<html slug="html_900">
|
||||
<!-- UTF-8 characters are acceptable… HTML entities are not -->
|
||||
<h1>Inline content…</h1>
|
||||
</html>
|
||||
</vertical>
|
||||
</randomize>
|
||||
</sequential>
|
||||
|
||||
Submodule common/test/phantom-jasmine deleted from a54d435b55
@@ -8,7 +8,7 @@ and acceptance tests.
|
||||
### Unit Tests
|
||||
|
||||
* Each test case should be concise: setup, execute, check, and teardown.
|
||||
If you find yourself writing tests with many steps, consider refactoring
|
||||
If you find yourself writing tests with many steps, consider refactoring
|
||||
the unit under tests into smaller units, and then testing those individually.
|
||||
|
||||
* As a rule of thumb, your unit tests should cover every code branch.
|
||||
@@ -16,19 +16,19 @@ the unit under tests into smaller units, and then testing those individually.
|
||||
* Mock or patch external dependencies.
|
||||
We use [voidspace mock](http://www.voidspace.org.uk/python/mock/).
|
||||
|
||||
* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
|
||||
* We unit test Python code (using [unittest](http://docs.python.org/2/library/unittest.html)) and
|
||||
Javascript (using [Jasmine](http://pivotal.github.io/jasmine/))
|
||||
|
||||
### Integration Tests
|
||||
* Test several units at the same time.
|
||||
Note that you can still mock or patch dependencies
|
||||
that are not under test! For example, you might test that
|
||||
`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
|
||||
that are not under test! For example, you might test that
|
||||
`LoncapaProblem`, `NumericalResponse`, and `CorrectMap` in the
|
||||
`capa` package work together, while still mocking out template rendering.
|
||||
|
||||
* Use integration tests to ensure that units are hooked up correctly.
|
||||
You do not need to test every possible input--that's what unit
|
||||
tests are for. Instead, focus on testing the "happy path"
|
||||
You do not need to test every possible input--that's what unit
|
||||
tests are for. Instead, focus on testing the "happy path"
|
||||
to verify that the components work together correctly.
|
||||
|
||||
* Many of our tests use the [Django test client](https://docs.djangoproject.com/en/dev/topics/testing/overview/) to simulate
|
||||
@@ -43,8 +43,8 @@ these tests simulate user interactions through the browser using
|
||||
|
||||
Overall, you want to write the tests that **maximize coverage**
|
||||
while **minimizing maintenance**.
|
||||
In practice, this usually means investing heavily
|
||||
in unit tests, which tend to be the most robust to changes in the code base.
|
||||
In practice, this usually means investing heavily
|
||||
in unit tests, which tend to be the most robust to changes in the code base.
|
||||
|
||||

|
||||
|
||||
@@ -53,13 +53,13 @@ and acceptance tests. Most of our tests are unit tests or integration tests.
|
||||
|
||||
## Test Locations
|
||||
|
||||
* Python unit and integration tests: Located in
|
||||
* Python unit and integration tests: Located in
|
||||
subpackages called `tests`.
|
||||
For example, the tests for the `capa` package are located in
|
||||
For example, the tests for the `capa` package are located in
|
||||
`common/lib/capa/capa/tests`.
|
||||
|
||||
* Javascript unit tests: Located in `spec` folders. For example,
|
||||
`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
|
||||
`common/lib/xmodule/xmodule/js/spec` and `{cms,lms}/static/coffee/spec`
|
||||
For consistency, you should use the same directory structure for implementation
|
||||
and test. For example, the test for `src/views/module.coffee`
|
||||
should be written in `spec/views/module_spec.coffee`.
|
||||
@@ -88,7 +88,7 @@ because the `capa` package handles problem XML.
|
||||
|
||||
Before running tests, ensure that you have all the dependencies. You can install dependencies using:
|
||||
|
||||
pip install -r requirements.txt
|
||||
rake install_prereqs
|
||||
|
||||
|
||||
## Running Python Unit tests
|
||||
@@ -101,7 +101,7 @@ You can run tests using `rake` commands. For example,
|
||||
|
||||
rake test
|
||||
|
||||
runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
|
||||
runs all the tests. It also runs `collectstatic`, which prepares the static files used by the site (for example, compiling Coffeescript to Javascript).
|
||||
|
||||
You can also run the tests without `collectstatic`, which tends to be faster:
|
||||
|
||||
@@ -117,12 +117,11 @@ xmodule can be tested independently, with this:
|
||||
|
||||
To run a single django test class:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
|
||||
rake test_lms[courseware.tests.tests:testViewAuth]
|
||||
|
||||
To run a single django test:
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
|
||||
|
||||
rake test_lms[courseware.tests.tests:TestViewAuth.test_dark_launch]
|
||||
|
||||
To run a single nose test file:
|
||||
|
||||
@@ -150,7 +149,7 @@ If the `phantomjs` binary is not on the path, set the `PHANTOMJS_PATH` environme
|
||||
|
||||
PHANTOMJS_PATH=/path/to/phantomjs rake phantomjs_jasmine_{lms,cms}
|
||||
|
||||
Once you have run the `rake` command, your browser should open to
|
||||
Once you have run the `rake` command, your browser should open to
|
||||
to `http://localhost/_jasmine/`, which displays the test results.
|
||||
|
||||
**Troubleshooting**: If you get an error message while running the `rake` task,
|
||||
@@ -163,7 +162,7 @@ Most of our tests use [Splinter](http://splinter.cobrateam.info/)
|
||||
to simulate UI browser interactions. Splinter, in turn,
|
||||
uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser.
|
||||
|
||||
**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
|
||||
**Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver)
|
||||
installed to run the tests in Chrome. The tests are confirmed to run
|
||||
with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver
|
||||
version r195636.
|
||||
@@ -184,13 +183,7 @@ To start the debugger on failure, add the `--pdb` option:
|
||||
To run tests faster by not collecting static files, you can use
|
||||
`rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`.
|
||||
|
||||
|
||||
**Troubleshooting**: If you get an error message that says something about harvest not being a command, you probably are missing a requirement.
|
||||
Try running:
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
**Note**: The acceptance tests can *not* currently run in parallel.
|
||||
**Note**: The acceptance tests can *not* currently run in parallel.
|
||||
|
||||
## Viewing Test Coverage
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ rake pylint > pylint.log || cat pylint.log
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Run the python unit tests
|
||||
rake test_cms[false] || TESTS_FAILED=1
|
||||
rake test_lms[false] || TESTS_FAILED=1
|
||||
rake test_cms || TESTS_FAILED=1
|
||||
rake test_lms || TESTS_FAILED=1
|
||||
rake test_common/lib/capa || TESTS_FAILED=1
|
||||
rake test_common/lib/xmodule || TESTS_FAILED=1
|
||||
|
||||
@@ -82,7 +82,7 @@ rake test_common/lib/xmodule || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_lms || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_cms || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_discussion || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_common/static/coffee || TESTS_FAILED=1
|
||||
|
||||
rake coverage:xml coverage:html
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
|
||||
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
|
||||
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from django_comment_client.models import Role
|
||||
from django_comment_common.models import Role
|
||||
from courseware.access import has_access
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_client.models import Role
|
||||
from django_comment_common.models import Role
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Enrollments.
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.models import assign_default_role
|
||||
from django_comment_common.models import assign_default_role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -7,7 +7,7 @@ Enrollments.
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.models import assign_default_role
|
||||
from django_comment_common.models import assign_default_role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_client.models import Role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -13,26 +13,4 @@ class Command(BaseCommand):
|
||||
raise CommandError("Too many arguments")
|
||||
course_id = args[0]
|
||||
|
||||
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
|
||||
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
|
||||
community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
|
||||
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
|
||||
|
||||
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
|
||||
"update_comment", "create_sub_comment", "unvote", "create_thread",
|
||||
"follow_commentable", "unfollow_commentable", "create_comment", ]:
|
||||
student_role.add_permission(per)
|
||||
|
||||
for per in ["edit_content", "delete_thread", "openclose_thread",
|
||||
"endorse_comment", "delete_comment", "see_all_cohorts"]:
|
||||
moderator_role.add_permission(per)
|
||||
|
||||
for per in ["manage_moderator"]:
|
||||
administrator_role.add_permission(per)
|
||||
|
||||
moderator_role.inherit_permissions(student_role)
|
||||
|
||||
# For now, Community TA == Moderator, except for the styling.
|
||||
community_ta_role.inherit_permissions(moderator_role)
|
||||
|
||||
administrator_role.inherit_permissions(moderator_role)
|
||||
seed_permissions_roles(course_id)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_common.models import Permission, Role
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
|
||||
@@ -1,64 +1 @@
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
FORUM_ROLE_ADMINISTRATOR = 'Administrator'
|
||||
FORUM_ROLE_MODERATOR = 'Moderator'
|
||||
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
|
||||
FORUM_ROLE_STUDENT = 'Student'
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False)
|
||||
users = models.ManyToManyField(User, related_name="roles")
|
||||
course_id = models.CharField(max_length=255, blank=True, db_index=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name + " for " + (self.course_id if self.course_id else "all courses")
|
||||
|
||||
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
|
||||
# since it's one-off and doesn't handle inheritance later
|
||||
if role.course_id and role.course_id != self.course_id:
|
||||
logging.warning("%s cannot inherit permissions from %s due to course_id inconsistency",
|
||||
self, role)
|
||||
for per in role.permissions.all():
|
||||
self.add_permission(per)
|
||||
|
||||
def add_permission(self, permission):
|
||||
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
|
||||
|
||||
def has_permission(self, permission):
|
||||
course = get_course_by_id(self.course_id)
|
||||
if self.name == FORUM_ROLE_STUDENT and \
|
||||
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
|
||||
(not course.forum_posts_allowed):
|
||||
return False
|
||||
|
||||
return self.permissions.filter(name=permission).exists()
|
||||
|
||||
|
||||
class Permission(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
|
||||
roles = models.ManyToManyField(Role, related_name="permissions")
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
# This file is intentionally blank. It has been moved to common/djangoapps/django_comment_common
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .models import Role, Permission
|
||||
from django_comment_common.models import Role, Permission
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.test import TestCase
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.permissions import has_permission
|
||||
from django_comment_client.models import Role
|
||||
from django_comment_common.models import Role
|
||||
|
||||
|
||||
class PermissionsTestCase(TestCase):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from factory import DjangoModelFactory
|
||||
from django_comment_client.models import Role, Permission
|
||||
from django_comment_common.models import Role, Permission
|
||||
|
||||
|
||||
class RoleFactory(DjangoModelFactory):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import django_comment_client.models as models
|
||||
import django_comment_common.models as models
|
||||
import django_comment_client.permissions as permissions
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.test import TestCase
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
|
||||
from django_comment_common.models import Role, Permission
|
||||
from factories import RoleFactory
|
||||
import django_comment_client.utils as utils
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.db import connection
|
||||
from django.http import HttpResponse
|
||||
from django.utils import simplejson
|
||||
from django_comment_client.models import Role
|
||||
from django_comment_common.models import Role
|
||||
from django_comment_client.permissions import check_permissions_by_view
|
||||
from xmodule.modulestore.exceptions import NoPathToItem
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.utils import has_forum_access
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from courseware.access import (has_access, get_access_group_name,
|
||||
course_beta_test_group_name)
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.models import StudentModule
|
||||
from django_comment_client.models import (Role,
|
||||
from django_comment_common.models import (Role,
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
FORUM_ROLE_MODERATOR,
|
||||
FORUM_ROLE_COMMUNITY_TA)
|
||||
|
||||
@@ -700,8 +700,7 @@ INSTALLED_APPS = (
|
||||
|
||||
# Discussion forums
|
||||
'django_comment_client',
|
||||
|
||||
# Student notes
|
||||
'django_comment_common',
|
||||
'notes',
|
||||
)
|
||||
|
||||
|
||||
@@ -36,7 +36,12 @@ PIPELINE_JS['spec'] = {
|
||||
}
|
||||
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/lms/jasmine')
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
|
||||
TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
|
||||
|
||||
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
|
||||
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
|
||||
|
||||
INSTALLED_APPS += ('django_jasmine', )
|
||||
INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// overflow-y: scroll;
|
||||
// }
|
||||
|
||||
body {
|
||||
background: rgb(250,250,250);
|
||||
html, body {
|
||||
background: $body-bg;
|
||||
font-family: $sans-serif;
|
||||
font-size: 1em;
|
||||
font-style: normal;
|
||||
@@ -61,20 +61,20 @@ p + p, ul + p, ol + p {
|
||||
|
||||
p {
|
||||
a:link, a:visited {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
font: normal 1em/1em $serif;
|
||||
text-decoration: none;
|
||||
@include transition(all, 0.1s, linear);
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
font: normal 1em/1em $sans-serif;
|
||||
text-decoration: none;
|
||||
@include transition(all, 0.1s, linear);
|
||||
@@ -87,8 +87,8 @@ a:link, a:visited {
|
||||
.content-wrapper {
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
background: $content-wrapper-bg;
|
||||
padding-bottom: ($baseline*2);
|
||||
background: rgb(255,255,255);
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -164,7 +164,7 @@ mark {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
@include linear-gradient(top, rgba(0, 0, 0, .1), rgba(0, 0, 0, .0));
|
||||
background-color: $pink;
|
||||
background-color: $site-status-color;
|
||||
box-shadow: 0 -1px 0 rgba(0, 0, 0, .3) inset;
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
@@ -1,39 +1,30 @@
|
||||
.faded-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
@include background-image($faded-hr-image-1);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-medium {
|
||||
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
|
||||
rgba(240,240,240, 1) 50%,
|
||||
rgba(240,240,240, 0)));
|
||||
@include background-image($faded-hr-image-4);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-hr-divider-light {
|
||||
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.8) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
@include background-image($faded-hr-image-5);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faded-vertical-divider {
|
||||
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1) 50%,
|
||||
rgba(200,200,200, 0)));
|
||||
@include background-image($faded-hr-image-1);
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.faded-vertical-divider-light {
|
||||
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
|
||||
rgba(255,255,255, 0.6) 50%,
|
||||
rgba(255,255,255, 0)));
|
||||
@include background-image($faded-hr-image-6);
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -66,14 +57,12 @@
|
||||
}
|
||||
|
||||
.fade-right-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
|
||||
rgba(200,200,200, 1)));
|
||||
@include background-image($faded-hr-image-2);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fade-left-hr-divider {
|
||||
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
|
||||
rgba(200,200,200, 0)));
|
||||
@include background-image($faded-hr-image-3);
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ $monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
|
||||
$body-font-family: $sans-serif;
|
||||
$serif: $georgia;
|
||||
|
||||
$body-font-size: em(14);
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$base-font-color: rgb(60,60,60);
|
||||
$baseFontColor: rgb(60,60,60);
|
||||
$base-font-color: rgb(60,60,60);
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
$very-light-text: #fff;
|
||||
|
||||
$white: rgb(255,255,255);
|
||||
$black: rgb(0,0,0);
|
||||
$blue: rgb(29,157,217);
|
||||
@@ -52,6 +60,66 @@ $baseFontColor: rgb(60,60,60);
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
$text-color: $dark-gray;
|
||||
|
||||
$body-font-family: $sans-serif;
|
||||
$body-font-size: em(14);
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$body-bg: rgb(250,250,250);
|
||||
$header-image: linear-gradient(-90deg, rgba(255,255,255, 1), rgba(230,230,230, 0.9));
|
||||
$header-bg: transparent;
|
||||
$courseware-header-image: linear-gradient(top, #fff, #eee);
|
||||
$courseware-header-bg: transparent;
|
||||
$footer-bg: transparent;
|
||||
$courseware-footer-border: none;
|
||||
$courseware-footer-shadow: none;
|
||||
$courseware-footer-margin: 0px;
|
||||
|
||||
$button-bg-image: linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%);
|
||||
$button-bg-color: transparent;
|
||||
$button-bg-hover-color: #fff;
|
||||
|
||||
$faded-hr-image-1: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1) 50%, rgba(200,200,200, 0));
|
||||
$faded-hr-image-2: linear-gradient(180deg, rgba(200,200,200, 0) 0%, rgba(200,200,200, 1));
|
||||
$faded-hr-image-3: linear-gradient(180deg, rgba(200,200,200, 1) 0%, rgba(200,200,200, 0));
|
||||
$faded-hr-image-4: linear-gradient(180deg, rgba(240,240,240, 0) 0%, rgba(240,240,240, 1) 50%, rgba(240,240,240, 0));
|
||||
$faded-hr-image-5: linear-gradient(180deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.8) 50%, rgba(255,255,255, 0));
|
||||
$faded-hr-image-6: linear-gradient(90deg, rgba(255,255,255, 0) 0%, rgba(255,255,255, 0.6) 50%, rgba(255,255,255, 0));
|
||||
|
||||
$dashboard-profile-header-image: linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245));
|
||||
$dashboard-profile-header-color: transparent;
|
||||
$dashboard-profile-color: rgb(252,252,252);
|
||||
$dot-color: $light-gray;
|
||||
|
||||
$content-wrapper-bg: rgb(255,255,255);
|
||||
$course-bg-color: #d6d6d6;
|
||||
$course-bg-image: url(../images/bg-texture.png);
|
||||
|
||||
$course-profile-bg: rgb(245,245,245);
|
||||
$course-header-bg: rgba(255,255,255, 0.93);
|
||||
|
||||
$border-color-1: rgb(190,190,190);
|
||||
$border-color-2: rgb(200,200,200);
|
||||
$border-color-3: rgb(100,100,100);
|
||||
$border-color-4: rgb(252,252,252);
|
||||
|
||||
$link-color: $blue;
|
||||
$link-hover: $pink;
|
||||
$selection-color-1: $pink;
|
||||
$selection-color-2: #444;
|
||||
$site-status-color: $pink;
|
||||
|
||||
$button-color: $blue;
|
||||
$button-archive-color: #eee;
|
||||
|
||||
$shadow-color: $blue;
|
||||
|
||||
$sidebar-chapter-bg-top: rgba(255, 255, 255, .6);
|
||||
$sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0);
|
||||
$sidebar-chapter-bg: #eee;
|
||||
$sidebar-active-image: linear-gradient(top, #e6e6e6, #d6d6d6);
|
||||
|
||||
$form-bg-color: #fff;
|
||||
$modal-bg-color: rgb(245,245,245);
|
||||
|
||||
//-----------------
|
||||
// CSS BG Images
|
||||
//-----------------
|
||||
$homepage-bg-image: '../images/homepage-bg.jpg';
|
||||
|
||||
$video-thumb-url: '../images/courses/video-thumb.jpg';
|
||||
@@ -117,7 +117,7 @@ div.info-wrapper {
|
||||
@include transition(all .2s);
|
||||
|
||||
h4 {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
padding-left: 30px;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
body {
|
||||
min-width: 980px;
|
||||
min-height: 100%;
|
||||
background: url(../images/bg-texture.png) #d6d6d6;
|
||||
background-image: $course-bg-image;
|
||||
background-color: $course-bg-color;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label {
|
||||
@@ -34,7 +35,7 @@ a {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $outer-border-color;
|
||||
background: #fff;
|
||||
background: $body-bg;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
}
|
||||
@@ -49,8 +50,8 @@ textarea,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
background: $body-bg;
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(0);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
|
||||
@include box-sizing(border-box);
|
||||
@@ -65,7 +66,7 @@ input[type="password"] {
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: lighten($blue, 20%);
|
||||
border-color: lighten($link-color, 20%);
|
||||
@include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
|
||||
outline: none;
|
||||
}
|
||||
@@ -94,7 +95,7 @@ img {
|
||||
}
|
||||
|
||||
::selection, ::-moz-selection, ::-webkit-selection {
|
||||
background: #444;
|
||||
background: $selection-color-2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ img {
|
||||
max-width: 350px;
|
||||
padding: 15px 20px 17px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #333;
|
||||
border: 1px solid $border-color-3;
|
||||
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .1), rgba(255, 255, 255, 0)) rgba(30, 30, 30, .92);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 1px 0 rgba(255, 255, 255, .1) inset;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
h1.top-header {
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
text-align: left;
|
||||
font-size: em(24);
|
||||
font-weight: 100;
|
||||
|
||||
@@ -2,7 +2,7 @@ section.course-index {
|
||||
@extend .sidebar;
|
||||
@extend .tran;
|
||||
@include border-radius(3px 0 0 3px);
|
||||
border-right: 1px solid #ddd;
|
||||
border-right: 1px solid $border-color-2;
|
||||
|
||||
#open_close_accordion {
|
||||
display: none;
|
||||
@@ -70,8 +70,8 @@ section.course-index {
|
||||
width: 100% !important;
|
||||
@include box-sizing(border-box);
|
||||
padding: 11px 14px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0));
|
||||
background-color: #eee;
|
||||
@include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom);
|
||||
background-color: $sidebar-chapter-bg;
|
||||
@include box-shadow(0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset);
|
||||
@include transition(background-color .1s);
|
||||
|
||||
@@ -169,9 +169,9 @@ section.course-index {
|
||||
}
|
||||
|
||||
> a {
|
||||
border: 1px solid #bbb;
|
||||
border: 1px solid $border-color-1;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .35) inset);
|
||||
@include linear-gradient(top, #e6e6e6, #d6d6d6);
|
||||
background: $sidebar-active-image;
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
|
||||
@@ -75,9 +75,9 @@ header.global.slim {
|
||||
|
||||
&#login {
|
||||
display: block;
|
||||
@include background-image(linear-gradient(-90deg, lighten($blue, 8%), lighten($blue, 5%) 50%, $blue 50%, darken($blue, 10%) 100%));
|
||||
@include background-image(linear-gradient(-90deg, lighten($link-color, 8%), lighten($link-color, 5%) 50%, $link-color 50%, darken($link-color, 10%) 100%));
|
||||
border: 1px solid transparent;
|
||||
border-color: darken($blue, 10%);
|
||||
border-color: darken($link-color, 10%);
|
||||
@include border-radius(3px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
@@ -97,7 +97,7 @@ header.global.slim {
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover, &.active {
|
||||
@include background-image(linear-gradient(-90deg, $blue, $blue 50%, $blue 50%, $blue 100%));
|
||||
@include background-image(linear-gradient(-90deg, $link-color, $link-color 50%, $link-color 50%, $link-color 100%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
footer {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border: $courseware-footer-border;
|
||||
box-shadow: $courseware-footer-shadow;
|
||||
margin-top: $courseware-footer-margin;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ section.wiki {
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $blue;
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ section.wiki {
|
||||
li {
|
||||
&.active {
|
||||
a {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
|
||||
.icon-view,
|
||||
.icon-home {
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
}
|
||||
|
||||
header.course-profile {
|
||||
background: rgb(245,245,245);
|
||||
@include background-image(url('/static/images/homepage-bg.jpg'));
|
||||
background: $course-profile-bg;
|
||||
@include background-image(url($homepage-bg-image));
|
||||
background-size: cover;
|
||||
@include box-shadow(0 1px 80px 0 rgba(0,0,0, 0.5));
|
||||
border-bottom: 1px solid rgb(100,100,100);
|
||||
border-bottom: 1px solid $border-color-3;
|
||||
@include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.1));
|
||||
height: 280px;
|
||||
margin-top: -69px;
|
||||
@@ -18,8 +18,8 @@
|
||||
width: 100%;
|
||||
|
||||
.intro-inner-wrapper {
|
||||
background: rgba(255,255,255, 0.93);
|
||||
border: 1px solid rgb(100,100,100);
|
||||
background: $course-header-bg;
|
||||
border: 1px solid $border-color-3;
|
||||
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
@@ -44,7 +44,7 @@
|
||||
z-index: 2;
|
||||
|
||||
> hgroup {
|
||||
border-bottom: 1px solid rgb(210,210,210);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
@@ -68,7 +68,7 @@
|
||||
text-transform: none;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
text-transform: none;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@
|
||||
width: flex-grid(12);
|
||||
|
||||
> a.find-courses, a.register {
|
||||
@include button(shiny, $blue);
|
||||
@include button(shiny, $button-color);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
|
||||
strong {
|
||||
@include button(shiny, $blue);
|
||||
@include button(shiny, $button-color);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
@@ -140,10 +140,10 @@
|
||||
}
|
||||
|
||||
span.register {
|
||||
background: lighten($blue, 20%);
|
||||
border: 1px solid $blue;
|
||||
background: $button-archive-color;
|
||||
border: 1px solid darken($button-archive-color, 50%);
|
||||
@include box-sizing(border-box);
|
||||
color: darken($blue, 20%);
|
||||
color: darken($button-archive-color, 50%);
|
||||
display: block;
|
||||
letter-spacing: 1px;
|
||||
padding: 10px 0px 8px;
|
||||
@@ -176,7 +176,7 @@
|
||||
z-index: 2;
|
||||
|
||||
.hero {
|
||||
border: 1px solid rgb(100,100,100);
|
||||
border: 1px solid $border-color-3;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -235,7 +235,7 @@
|
||||
@include clearfix;
|
||||
|
||||
nav {
|
||||
border-bottom: 1px solid rgb(220,220,220);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
margin: 40px 0;
|
||||
@@ -262,7 +262,7 @@
|
||||
}
|
||||
|
||||
&:hover, &.active {
|
||||
border-color: rgb(200,200,200);
|
||||
border-color: $border-color-2;
|
||||
color: $base-font-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -296,7 +296,7 @@
|
||||
|
||||
.teacher-image {
|
||||
background: rgb(255,255,255);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
border: 1px solid $border-color-2;
|
||||
height: 115px;
|
||||
float: left;
|
||||
margin: 0 15px 0px 0;
|
||||
@@ -351,7 +351,7 @@
|
||||
|
||||
> section {
|
||||
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
|
||||
border: 1px solid rgb(200,200,200);
|
||||
border: 1px solid $border-color-2;
|
||||
|
||||
&.course-summary {
|
||||
padding: 16px 20px 30px;
|
||||
@@ -401,7 +401,7 @@
|
||||
}
|
||||
|
||||
a.university-name {
|
||||
border-right: 1px solid rgb(200,200,200);
|
||||
border-right: 1px solid $border-color-2;
|
||||
color: $base-font-color;
|
||||
font-family: $sans-serif;
|
||||
font-style: italic;
|
||||
@@ -498,12 +498,12 @@
|
||||
|
||||
li {
|
||||
@include clearfix;
|
||||
border-bottom: 1px dotted rgb(220,220,220);
|
||||
border-bottom: 1px dotted $border-color-2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&.prerequisites {
|
||||
border: 1px solid rgb(220,220,220);
|
||||
border: 1px solid $border-color-2;
|
||||
margin: 0 -10px 0;
|
||||
padding: 10px;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
.find-courses, .university-profile {
|
||||
background: rgb(252,252,252);
|
||||
background: $course-profile-bg;
|
||||
padding-bottom: 60px;
|
||||
|
||||
header.search {
|
||||
background: rgb(240,240,240);
|
||||
background: $course-profile-bg;
|
||||
background-size: cover;
|
||||
@include background-image(url($homepage-bg-image));
|
||||
background-position: center top !important;
|
||||
border-bottom: 1px solid rgb(100,100,100);
|
||||
border-bottom: 1px solid $border-color-3;
|
||||
@include box-shadow(inset 0 -1px 8px 0 rgba(0,0,0, 0.2), inset 0 1px 12px 0 rgba(0,0,0, 0.3));
|
||||
height: 430px;
|
||||
margin-top: -69px;
|
||||
@@ -24,8 +25,8 @@
|
||||
|
||||
> hgroup {
|
||||
background: #FFF;
|
||||
background: rgba(255,255,255, 0.93);
|
||||
border: 1px solid rgb(100,100,100);
|
||||
background: $course-header-bg;
|
||||
border: 1px solid $border-color-3;
|
||||
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
|
||||
padding: 20px 30px;
|
||||
position: relative;
|
||||
@@ -83,7 +84,7 @@
|
||||
}
|
||||
|
||||
section.message {
|
||||
border-top: 1px solid rgb(220,220,220);
|
||||
border-top: 1px solid $border-color-2;
|
||||
@include clearfix;
|
||||
margin-top: 20px;
|
||||
padding-top: 60px;
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
width: flex-grid(3);
|
||||
|
||||
header.profile {
|
||||
@include background-image(linear-gradient(-90deg, rgb(255,255,255), rgb(245,245,245)));
|
||||
border: 1px solid rgb(200,200,200);
|
||||
@include background-image($dashboard-profile-header-image);
|
||||
background-color: $dashboard-profile-header-color;
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(12);
|
||||
@@ -53,8 +54,8 @@
|
||||
padding: 0px 10px;
|
||||
|
||||
> ul {
|
||||
background: rgb(252,252,252);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
background: $dashboard-profile-color;
|
||||
border: 1px solid $border-color-2;
|
||||
border-top: none;
|
||||
//@include border-bottom-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
li {
|
||||
@include clearfix;
|
||||
border-bottom: 1px dotted rgb(220,220,220);
|
||||
border-bottom: 1px dotted $border-color-2;
|
||||
list-style: none;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 17px;
|
||||
@@ -128,8 +129,8 @@
|
||||
.news-carousel {
|
||||
@include clearfix;
|
||||
margin: 30px 10px 0;
|
||||
border: 1px solid rgb(200,200,200);
|
||||
background: rgb(252,252,252);
|
||||
border: 1px solid $border-color-2;
|
||||
background: $dashboard-profile-color;
|
||||
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
|
||||
|
||||
* {
|
||||
@@ -156,14 +157,14 @@
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 11px;
|
||||
background: $light-gray;
|
||||
background: $dot-color;
|
||||
|
||||
&:hover {
|
||||
background: #ccc;
|
||||
background: $lighter-base-font-color;
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: $blue;
|
||||
background: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid $light-gray;
|
||||
border: 1px solid $border-color-1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@
|
||||
width: flex-grid(9);
|
||||
|
||||
> header {
|
||||
border-bottom: 1px solid rgb(210,210,210);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -246,8 +247,9 @@
|
||||
|
||||
a {
|
||||
background: rgb(240,240,240);
|
||||
@include background-image(linear-gradient(-90deg, rgb(245,245,245) 0%, rgb(243,243,243) 50%, rgb(237,237,237) 50%, rgb(235,235,235) 100%));
|
||||
border: 1px solid rgb(220,220,220);
|
||||
@include background-image($button-bg-image);
|
||||
background-color: $button-bg-color;
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 1px 8px 0 rgba(0,0,0, 0.1));
|
||||
@include box-sizing(border-box);
|
||||
@@ -260,7 +262,7 @@
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.6);
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,7 @@
|
||||
margin-right: flex-gutter();
|
||||
margin-bottom: 50px;
|
||||
padding-bottom: 50px;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
border-bottom: 1px solid $border-color-1;
|
||||
position: relative;
|
||||
width: flex-grid(12);
|
||||
z-index: 20;
|
||||
@@ -343,7 +345,7 @@
|
||||
|
||||
.course-status {
|
||||
background: $yellow;
|
||||
border: 1px solid rgb(200,200,200);
|
||||
border: 1px solid $border-color-2;
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
margin-top: 17px;
|
||||
margin-right: flex-gutter();
|
||||
@@ -362,7 +364,7 @@
|
||||
|
||||
.course-status-completed {
|
||||
background: #ccc;
|
||||
color: #fff;
|
||||
color: $very-light-text;
|
||||
|
||||
p {
|
||||
color: #222;
|
||||
@@ -374,7 +376,7 @@
|
||||
}
|
||||
|
||||
.enter-course {
|
||||
@include button(simple, $blue);
|
||||
@include button(simple, $button-color);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
@@ -386,7 +388,7 @@
|
||||
margin-top: 16px;
|
||||
|
||||
&.archived {
|
||||
@include button(simple, #eee);
|
||||
@include button(simple, $button-archive-color);
|
||||
font: normal 15px/1.6rem $sans-serif;
|
||||
padding: 6px 32px 7px;
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
}
|
||||
|
||||
> header {
|
||||
background: rgb(255,255,255);
|
||||
@include background-image(url('/static/images/homepage-bg.jpg'));
|
||||
background: $dashboard-profile-color;
|
||||
@include background-image(url($homepage-bg-image));
|
||||
background-size: cover;
|
||||
border-bottom: 1px solid rgb(80,80,80);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.9), inset 0 -1px 5px 0 rgba(0,0,0, 0.1));
|
||||
border-bottom: 1px solid $border-color-3;
|
||||
@include box-shadow(0 1px 0 0 $course-header-bg, inset 0 -1px 5px 0 rgba(0,0,0, 0.1));
|
||||
@include clearfix;
|
||||
height: 460px;
|
||||
margin-top: -69px;
|
||||
overflow: hidden;
|
||||
margin-top: -69px;
|
||||
padding: 0px;
|
||||
width: flex-grid(12);
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
.title {
|
||||
background: #FFF;
|
||||
background: rgba(255,255,255, 0.93);
|
||||
border: 1px solid rgb(100,100,100);
|
||||
background: $course-header-bg;
|
||||
border: 1px solid $border-color-3;
|
||||
@include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
|
||||
@include box-sizing(border-box);
|
||||
min-height: 120px;
|
||||
@@ -80,8 +80,8 @@
|
||||
|
||||
.media {
|
||||
background: #FFF;
|
||||
background: rgba(255,255,255, 0.93);
|
||||
border: 1px solid rgb(100,100,100);
|
||||
background: $course-header-bg;
|
||||
border: 1px solid $border-color-3;
|
||||
border-left: 0;
|
||||
@include box-sizing(border-box);
|
||||
// @include box-shadow(0 4px 25px 0 rgba(0,0,0, 0.5));
|
||||
@@ -101,7 +101,7 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: url('../images/courses/video-thumb.jpg') center no-repeat;
|
||||
background: url($video-thumb-url) center no-repeat;
|
||||
@include background-size(cover);
|
||||
|
||||
.play-intro {
|
||||
@@ -164,9 +164,9 @@
|
||||
|
||||
> h2 {
|
||||
@include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230)));
|
||||
border: 1px solid rgb(200,200,200);
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(4px);
|
||||
border-top-color: rgb(190,190,190);
|
||||
border-top-color: $border-color-1;
|
||||
@include box-shadow(inset 0 0 0 1px rgba(255,255,255, 0.4), 0 0px 12px 0 rgba(0,0,0, 0.2));
|
||||
color: $lighter-base-font-color;
|
||||
letter-spacing: 1px;
|
||||
@@ -180,7 +180,7 @@
|
||||
}
|
||||
|
||||
.university-partners {
|
||||
border-bottom: 1px solid rgb(210,210,210);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
margin-bottom: 0px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -366,13 +366,13 @@
|
||||
}
|
||||
|
||||
.more-info {
|
||||
border: 1px solid rgb(200,200,200);
|
||||
border: 1px solid $border-color-2;
|
||||
margin-bottom: 80px;
|
||||
width: flex-grid(12);
|
||||
|
||||
header {
|
||||
@include background-image(linear-gradient(-90deg, rgb(250,250,250), rgb(230,230,230)));
|
||||
border-bottom: 1px solid rgb(200,200,200);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
@include clearfix;
|
||||
padding: 10px 20px 8px;
|
||||
position: relative;
|
||||
@@ -415,14 +415,14 @@
|
||||
width: flex-grid(12);
|
||||
|
||||
.blog-posts {
|
||||
border-bottom: 1px solid rgb(220,220,220);
|
||||
border-bottom: 1px solid $border-color-2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
@include clearfix;
|
||||
|
||||
> article {
|
||||
border: 1px dotted transparent;
|
||||
border-color: rgb(220,220,220);
|
||||
border-color: $border-color-2;
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
float: left;
|
||||
@@ -432,8 +432,8 @@
|
||||
width: flex-grid(4);
|
||||
|
||||
&:hover {
|
||||
background: rgb(248,248,248);
|
||||
border: 1px solid rgb(220,220,220);
|
||||
background: $body-bg;
|
||||
border: 1px solid $border-color-2;
|
||||
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.1));
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@
|
||||
}
|
||||
|
||||
.post-graphics {
|
||||
border: 1px solid rgb(190,190,190);
|
||||
border: 1px solid $border-color-1;
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
float: left;
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
}
|
||||
|
||||
.course {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(180,180,180);
|
||||
background: $body-bg;
|
||||
border: 1px solid $border-color-1;
|
||||
@include border-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 10px 0 rgba(0,0,0, 0.15), inset 0 0 0 1px rgba(255,255,255, 0.9));
|
||||
@@ -42,7 +42,7 @@
|
||||
@include transition(all, 0.15s, linear);
|
||||
|
||||
.status {
|
||||
background: $blue;
|
||||
background: $link-color;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
left: 10px;
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.status:after {
|
||||
border-bottom: 6px solid shade($blue, 50%);
|
||||
border-bottom: 6px solid shade($link-color, 50%);
|
||||
border-right: 6px solid transparent;
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
border: 1px solid rgba(255,255,255, 1);
|
||||
border: 1px solid $border-color-4;
|
||||
height: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
@@ -116,12 +116,12 @@
|
||||
text-decoration: none;
|
||||
|
||||
.info-link {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
// }
|
||||
|
||||
.info {
|
||||
background: rgb(255,255,255);
|
||||
background: $content-wrapper-bg;
|
||||
height: 220px + 130px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
@@ -221,14 +221,14 @@
|
||||
width: 100%;
|
||||
|
||||
.university {
|
||||
border-right: 1px solid rgb(200,200,200);
|
||||
border-right: 1px solid $border-color-2;
|
||||
color: $lighter-base-font-color;
|
||||
letter-spacing: 1px;
|
||||
margin-right: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +240,9 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgb(245,245,245);
|
||||
border-color: rgb(170,170,170);
|
||||
@include box-shadow(0 1px 16px 0 rgba($blue, 0.4));
|
||||
background: $course-profile-bg;
|
||||
border-color: $border-color-1;
|
||||
@include box-shadow(0 1px 16px 0 rgba($shadow-color, 0.4));
|
||||
|
||||
.info {
|
||||
top: -150px;
|
||||
|
||||
@@ -159,4 +159,4 @@
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="tel"] {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
background: $form-bg-color;
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
|
||||
@include box-sizing(border-box);
|
||||
@@ -31,8 +31,8 @@ input[type="tel"] {
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: lighten($blue, 20%);
|
||||
@include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
|
||||
border-color: darken($button-archive-color, 50%);
|
||||
@include box-shadow(0 0 6px 0 darken($button-archive-color, 50%), inset 0 0 4px 0 rgba(0,0,0, 0.15));
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ input[type="button"],
|
||||
button,
|
||||
.button {
|
||||
@include border-radius(3px);
|
||||
@include button(shiny, $blue);
|
||||
@include button(shiny, $button-color);
|
||||
font: normal 1.2rem/1.6rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 4px 20px;
|
||||
|
||||
@@ -54,8 +54,7 @@ header.global {
|
||||
|
||||
li.secondary {
|
||||
> a {
|
||||
color: $lighter-base-font-color;
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
display: block;
|
||||
font-family: $sans-serif;
|
||||
@include inline-block;
|
||||
@@ -78,9 +77,9 @@ header.global {
|
||||
margin-right: 5px;
|
||||
|
||||
> a {
|
||||
@include background-image(linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
|
||||
border: 1px solid transparent;
|
||||
border-color: rgb(200,200,200);
|
||||
@include background-image($button-bg-image);
|
||||
background-color: $button-bg-color;
|
||||
border: 1px solid $border-color-2;
|
||||
@include border-radius(3px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
@@ -101,7 +100,7 @@ header.global {
|
||||
}
|
||||
|
||||
&:hover, &.active {
|
||||
background: #FFF;
|
||||
background: $button-bg-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,10 +158,10 @@ header.global {
|
||||
}
|
||||
|
||||
ul.dropdown-menu {
|
||||
background: rgb(252,252,252);
|
||||
background: $border-color-4;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0 2px 24px 0 rgba(0,0,0, 0.3));
|
||||
border: 1px solid rgb(100,100,100);
|
||||
border: 1px solid $border-color-3;
|
||||
display: none;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
@@ -178,12 +177,12 @@ header.global {
|
||||
&::before {
|
||||
background: transparent;
|
||||
border: {
|
||||
top: 6px solid rgba(252,252,252, 1);
|
||||
right: 6px solid rgba(252,252,252, 1);
|
||||
top: 6px solid $border-color-4;
|
||||
right: 6px solid $border-color-4;
|
||||
bottom: 6px solid transparent;
|
||||
left: 6px solid transparent;
|
||||
}
|
||||
@include box-shadow(1px 0 0 0 rgb(0,0,0), 0 -1px 0 0 rgb(0,0,0));
|
||||
@include box-shadow(1px 0 0 0 $border-color-3, 0 -1px 0 0 $border-color-3);
|
||||
content: "";
|
||||
display: block;
|
||||
height: 0px;
|
||||
@@ -196,7 +195,7 @@ header.global {
|
||||
|
||||
li {
|
||||
display: block;
|
||||
border-top: 1px dotted rgba(200,200,200, 1);
|
||||
border-top: 1px dotted $border-color-2;
|
||||
@include box-shadow(inset 0 1px 0 0 rgba(255,255,255, 0.05));
|
||||
|
||||
&:first-child {
|
||||
@@ -208,7 +207,7 @@ header.global {
|
||||
border: 1px solid transparent;
|
||||
@include border-radius(3px);
|
||||
@include box-sizing(border-box);
|
||||
color: $blue;
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 5px 0px;
|
||||
@@ -328,4 +327,4 @@ header.global {
|
||||
text-decoration: none;
|
||||
color: $m-blue-s1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
background: rgb(245,245,245);
|
||||
background: $modal-bg-color;
|
||||
@include border-radius(0px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.9);
|
||||
@include box-shadow(inset 0 1px 0 0 rgba(255, 255, 255, 0.7));
|
||||
@@ -149,7 +149,7 @@
|
||||
}
|
||||
|
||||
label {
|
||||
color: #646464;
|
||||
color: $text-color;
|
||||
|
||||
&.field-error {
|
||||
display: block;
|
||||
|
||||
3
lms/templates/email_change_failed.html
Normal file
3
lms/templates/email_change_failed.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>E-mail change failed.</h1>
|
||||
|
||||
<p>We were unable to send a confirmation email to ${email}</p>
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"coffee-script": "1.6.X",
|
||||
"phantom-jasmine": "0.1.0"
|
||||
"phantom-jasmine": "0.1.0",
|
||||
"jasmine-reporters": "0.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
4
pylintrc
4
pylintrc
@@ -110,7 +110,9 @@ generated-members=
|
||||
get_url,
|
||||
size,
|
||||
content,
|
||||
status_code
|
||||
status_code,
|
||||
# For factory_body factories
|
||||
create
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
@@ -48,6 +48,7 @@ def template_jasmine_runner(lib)
|
||||
sh("node_modules/.bin/coffee -c #{coffee_files.join(' ')}")
|
||||
end
|
||||
phantom_jasmine_path = File.expand_path("node_modules/phantom-jasmine")
|
||||
jasmine_reporters_path = File.expand_path("node_modules/jasmine-reporters")
|
||||
common_js_root = File.expand_path("common/static/js")
|
||||
common_coffee_root = File.expand_path("common/static/coffee/src")
|
||||
|
||||
@@ -58,6 +59,7 @@ def template_jasmine_runner(lib)
|
||||
js_specs = Dir[spec_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
|
||||
js_source = Dir[src_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
|
||||
|
||||
report_dir = report_dir_path("#{lib}/jasmine")
|
||||
template = ERB.new(File.read("common/templates/jasmine/jasmine_test_runner.html.erb"))
|
||||
template_output = "#{lib}/jasmine_test_runner.html"
|
||||
File.open(template_output, 'w') do |f|
|
||||
@@ -66,6 +68,11 @@ def template_jasmine_runner(lib)
|
||||
yield File.expand_path(template_output)
|
||||
end
|
||||
|
||||
def run_phantom_js(url)
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
sh("#{phantomjs} node_modules/jasmine-reporters/test/phantomjs-testrunner.js #{url}")
|
||||
end
|
||||
|
||||
[:lms, :cms].each do |system|
|
||||
desc "Open jasmine tests for #{system} in your default browser"
|
||||
task "browse_jasmine_#{system}" => :assets do
|
||||
@@ -78,14 +85,16 @@ end
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for #{system} from the console"
|
||||
task "phantomjs_jasmine_#{system}" => :assets do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
django_for_jasmine(system, false) do |jasmine_url|
|
||||
sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
|
||||
run_phantom_js(jasmine_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
|
||||
STATIC_JASMINE_TESTS = Dir["common/lib/*"].select{|lib| File.directory?(lib)}
|
||||
STATIC_JASMINE_TESTS << 'common/static/coffee'
|
||||
|
||||
STATIC_JASMINE_TESTS.each do |lib|
|
||||
desc "Open jasmine tests for #{lib} in your default browser"
|
||||
task "browse_jasmine_#{lib}" do
|
||||
template_jasmine_runner(lib) do |f|
|
||||
@@ -97,26 +106,14 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for #{lib} from the console"
|
||||
task "phantomjs_jasmine_#{lib}" do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
template_jasmine_runner(lib) do |f|
|
||||
sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
|
||||
run_phantom_js(f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Open jasmine tests for discussion in your default browser"
|
||||
task "browse_jasmine_discussion" do
|
||||
template_jasmine_runner("common/static/coffee") do |f|
|
||||
sh("python -m webbrowser -t 'file://#{f}'")
|
||||
puts "Press ENTER to terminate".red
|
||||
$stdin.gets
|
||||
end
|
||||
end
|
||||
task "browse_jasmine_discussion" => "browse_jasmine_common/static/coffee"
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for discussion from the console"
|
||||
task "phantomjs_jasmine_discussion" do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
template_jasmine_runner("common/static/coffee") do |f|
|
||||
sh("#{phantomjs} node_modules/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
|
||||
end
|
||||
end
|
||||
task "phantomjs_jasmine_discussion" => "phantomjs_jasmine_common/static/coffee"
|
||||
|
||||
@@ -31,6 +31,7 @@ task :install_python_prereqs => "ws:migrate" do
|
||||
unchanged = 'Python requirements unchanged, nothing to install'
|
||||
when_changed(unchanged, ['requirements/**/*'], [site_packages_dir]) do
|
||||
ENV['PIP_DOWNLOAD_CACHE'] ||= '.pip_download_cache'
|
||||
sh('pip install --exists-action w -r requirements/edx/pre.txt')
|
||||
sh('pip install --exists-action w -r requirements/edx/base.txt')
|
||||
sh('pip install --exists-action w -r requirements/edx/post.txt')
|
||||
# requirements/private.txt is used to install our libs as
|
||||
|
||||
@@ -12,10 +12,11 @@ def run_under_coverage(cmd, root)
|
||||
return cmd
|
||||
end
|
||||
|
||||
def run_tests(system, report_dir, stop_on_failure=true)
|
||||
def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
|
||||
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
|
||||
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
|
||||
cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
|
||||
test_id = dirs.join(' ') if test_id.nil? or test_id == ''
|
||||
cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id)
|
||||
sh(run_under_coverage(cmd, system)) do |ok, res|
|
||||
if !ok and stop_on_failure
|
||||
abort "Test failed!"
|
||||
@@ -25,6 +26,16 @@ def run_tests(system, report_dir, stop_on_failure=true)
|
||||
end
|
||||
|
||||
def run_acceptance_tests(system, report_dir, harvest_args)
|
||||
# HACK: Since now the CMS depends on the existence of some database tables
|
||||
# that used to be in LMS (Role/Permissions for Forums) we need to make
|
||||
# sure the acceptance tests create/migrate the database tables
|
||||
# that are represented in the LMS. We might be able to address this by moving
|
||||
# out the migrations from lms/django_comment_client, but then we'd have to
|
||||
# repair all the existing migrations from the upgrade tables in the DB.
|
||||
if system == :cms
|
||||
sh(django_admin('lms', 'acceptance', 'syncdb', '--noinput'))
|
||||
sh(django_admin('lms', 'acceptance', 'migrate', '--noinput'))
|
||||
end
|
||||
sh(django_admin(system, 'acceptance', 'syncdb', '--noinput'))
|
||||
sh(django_admin(system, 'acceptance', 'migrate', '--noinput'))
|
||||
sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args))
|
||||
@@ -44,13 +55,13 @@ TEST_TASK_DIRS = []
|
||||
|
||||
# Per System tasks
|
||||
desc "Run all django tests on our djangoapps for the #{system}"
|
||||
task "test_#{system}", [:stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
|
||||
task "test_#{system}", [:test_id, :stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
|
||||
|
||||
# Have a way to run the tests without running collectstatic -- useful when debugging without
|
||||
# messing with static files.
|
||||
task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args|
|
||||
args.with_defaults(:stop_on_failure => 'true')
|
||||
run_tests(system, report_dir, args.stop_on_failure)
|
||||
task "fasttest_#{system}", [:test_id, :stop_on_failure] => [report_dir, :install_prereqs, :predjango] do |t, args|
|
||||
args.with_defaults(:stop_on_failure => 'true', :test_id => nil)
|
||||
run_tests(system, report_dir, args.test_id, args.stop_on_failure)
|
||||
end
|
||||
|
||||
# Run acceptance tests
|
||||
@@ -100,7 +111,7 @@ end
|
||||
|
||||
task :test do
|
||||
TEST_TASK_DIRS.each do |dir|
|
||||
Rake::Task["test_#{dir}"].invoke(false)
|
||||
Rake::Task["test_#{dir}"].invoke(nil, false)
|
||||
end
|
||||
|
||||
if $failed_tests > 0
|
||||
|
||||
@@ -29,7 +29,6 @@ mako==0.7.3
|
||||
Markdown==2.2.1
|
||||
networkx==1.7
|
||||
nltk==2.0.4
|
||||
numpy==1.6.2
|
||||
paramiko==1.9.0
|
||||
path.py==3.0.1
|
||||
Pillow==1.7.8
|
||||
@@ -43,6 +42,7 @@ python-openid==2.2.5
|
||||
pytz==2012h
|
||||
PyYAML==3.10
|
||||
requests==0.14.2
|
||||
scipy==0.11.0
|
||||
Shapely==1.2.16
|
||||
sorl-thumbnail==11.12
|
||||
South==0.7.6
|
||||
@@ -71,7 +71,7 @@ transifex-client==0.8
|
||||
coverage==3.6
|
||||
factory_boy==2.0.2
|
||||
lettuce==0.2.16
|
||||
mock==0.8.0
|
||||
mock==1.0.1
|
||||
nosexcover==1.0.7
|
||||
pep8==1.4.5
|
||||
pylint==0.28
|
||||
@@ -82,3 +82,5 @@ django_nose==1.1
|
||||
django-jasmine==0.3.2
|
||||
django_debug_toolbar
|
||||
django-debug-toolbar-mongo
|
||||
|
||||
git+https://github.com/mfogel/django-settings-context-processor.git
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
|
||||
-e git+https://github.com/edx/codejail.git@07494f1#egg=codejail
|
||||
-e git+https://github.com/edx/codejail.git@72cf791#egg=codejail
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
|
||||
# This must be installed after distribute 0.6.28
|
||||
MySQL-python==1.2.4c1
|
||||
|
||||
# This must be installed after numpy
|
||||
scipy==0.11.0
|
||||
# This must be installed after distribute has been updated.
|
||||
MySQL-python==1.2.4
|
||||
|
||||
3
requirements/edx/pre.txt
Normal file
3
requirements/edx/pre.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# Numpy and scipy can't be installed in the same pip run.
|
||||
# Install numpy before other things to help resolve the problem.
|
||||
numpy==1.6.2
|
||||
Reference in New Issue
Block a user