Commit 799b938d by Jean Manuel Nater

Merge branch 'master' into jnater/courseware_tests

parents 1b344e4d a28451ab
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Blades: Small UX fix on capa multiple-choice problems. Make labels only
as wide as the text to reduce accidental choice selections.
Studio: Remove XML from the video component editor. All settings are Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata. moved to be edited as metadata.
......
...@@ -27,7 +27,7 @@ def i_am_on_advanced_course_settings(step): ...@@ -27,7 +27,7 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
css = 'a.%s-button' % name.lower() css = 'a.action-%s' % name.lower()
# Save was clicked if either the save notification bar is gone, or we have a error notification # Save was clicked if either the save notification bar is gone, or we have a error notification
# overlaying it (expected in the case of typing Object into display_name). # overlaying it (expected in the case of typing Object into display_name).
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true from nose.tools import assert_true
from nose.tools import assert_equal
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
...@@ -13,8 +12,13 @@ import time ...@@ -13,8 +12,13 @@ import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
_COURSE_NAME = 'Robot Super Course'
_COURSE_NUM = '999'
_COURSE_ORG = 'MITx'
########### STEP HELPERS ############## ########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @step('I (?:visit|access|open) the Studio homepage$')
def i_visit_the_studio_homepage(_step): def i_visit_the_studio_homepage(_step):
# To make this go to port 8001, put # To make this go to port 8001, put
...@@ -54,6 +58,7 @@ def i_have_opened_a_new_course(_step): ...@@ -54,6 +58,7 @@ def i_have_opened_a_new_course(_step):
####### HELPER FUNCTIONS ############## ####### HELPER FUNCTIONS ##############
def open_new_course(): def open_new_course():
world.clear_courses() world.clear_courses()
create_studio_user()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
...@@ -73,10 +78,11 @@ def create_studio_user( ...@@ -73,10 +78,11 @@ def create_studio_user(
registration.register(studio_user) registration.register(studio_user)
registration.activate() registration.activate()
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name=_COURSE_NAME,
org='MITx', org=_COURSE_ORG,
num='101'): num=_COURSE_NUM):
world.css_fill('.new-course-name', name) world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org) world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num) world.css_fill('.new-course-number', num)
...@@ -85,10 +91,7 @@ def fill_in_course_info( ...@@ -85,10 +91,7 @@ def fill_in_course_info(
def log_into_studio( def log_into_studio(
uname='robot', uname='robot',
email='robot+studio@edx.org', email='robot+studio@edx.org',
password='test', password='test'):
is_staff=False):
create_studio_user(uname=uname, email=email, is_staff=is_staff)
world.browser.cookies.delete() world.browser.cookies.delete()
world.visit('/') world.visit('/')
...@@ -106,14 +109,14 @@ def log_into_studio( ...@@ -106,14 +109,14 @@ def log_into_studio(
def create_a_course(): def create_a_course():
world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME)
# Add the user to the instructor group of the course # Add the user to the instructor group of the course
# so they will have the permissions to see it in studio # so they will have the permissions to see it in studio
g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course') course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_")))
u = get_user_by_email('robot+studio@edx.org') user = get_user_by_email('robot+studio@edx.org')
u.groups.add(g) user.groups.add(course)
u.save() user.save()
world.browser.reload() world.browser.reload()
course_link_css = 'span.class-name' course_link_css = 'span.class-name'
......
Feature: Course Team
As a course author, I want to be able to add others to my team
Scenario: Users can add other users
Given I have opened a new course in Studio
And the user "alice" exists
And I am viewing the course team settings
When I add "alice" to the course team
And "alice" logs in
Then she does see the course on her page
Scenario: Added users cannot delete or add other users
Given I have opened a new course in Studio
And the user "bob" exists
And I am viewing the course team settings
When I add "bob" to the course team
And "bob" logs in
Then he cannot delete users
And he cannot add users
Scenario: Users can delete other users
Given I have opened a new course in Studio
And the user "carol" exists
And I am viewing the course team settings
When I add "carol" to the course team
And I delete "carol" from the course team
And "carol" logs in
Then she does not see the course on her page
Scenario: Users cannot add users that do not exist
Given I have opened a new course in Studio
And I am viewing the course team settings
When I add "dennis" to the course team
Then I should see "Could not find user by email address" somewhere on the page
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from common import create_studio_user, log_into_studio, _COURSE_NAME
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
@step(u'I am viewing the course team settings')
def view_grading_settings(_step):
world.click_course_settings()
link_css = 'li.nav-course-settings-team a'
world.css_click(link_css)
@step(u'the user "([^"]*)" exists$')
def create_other_user(_step, name):
create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
@step(u'I add "([^"]*)" to the course team')
def add_other_user(_step, name):
new_user_css = 'a.new-user-button'
world.css_click(new_user_css)
email_css = 'input.email-input'
f = world.css_find(email_css)
f._element.send_keys(name, EMAIL_EXTENSION)
confirm_css = '#add_user'
world.css_click(confirm_css)
@step(u'I delete "([^"]*)" from the course team')
def delete_other_user(_step, name):
to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION)
world.css_click(to_delete_css)
@step(u'"([^"]*)" logs in$')
def other_user_login(_step, name):
log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION)
@step(u's?he does( not)? see the course on (his|her) page')
def see_course(_step, doesnt_see_course, gender):
class_css = 'span.class-name'
all_courses = world.css_find(class_css)
all_names = [item.html for item in all_courses]
if doesnt_see_course:
assert not _COURSE_NAME in all_names
else:
assert _COURSE_NAME in all_names
@step(u's?he cannot delete users')
def cannot_delete(_step):
to_delete_css = 'a.remove-user'
assert world.is_css_not_present(to_delete_css)
@step(u's?he cannot add users')
def cannot_add(_step):
add_css = 'a.new-user'
assert world.is_css_not_present(add_css)
Feature: Course updates
As a course author, I want to be able to provide updates to my students
Scenario: Users can add updates
Given I have opened a new course in Studio
And I go to the course updates page
When I add a new update with the text "Hello"
Then I should see the update "Hello"
Scenario: Users can edit updates
Given I have opened a new course in Studio
And I go to the course updates page
When I add a new update with the text "Hello"
And I modify the text to "Goodbye"
Then I should see the update "Goodbye"
Scenario: Users can delete updates
Given I have opened a new course in Studio
And I go to the course updates page
And I add a new update with the text "Hello"
When I will confirm all alerts
And I delete the update
Then I should not see the update "Hello"
Scenario: Users can edit update dates
Given I have opened a new course in Studio
And I go to the course updates page
And I add a new update with the text "Hello"
When I edit the date to "June 1, 2013"
Then I should see the date "June 1, 2013"
Scenario: Users can change handouts
Given I have opened a new course in Studio
And I go to the course updates page
When I modify the handout to "<ol>Test</ol>"
Then I see the handout "Test"
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from selenium.webdriver.common.keys import Keys
from common import type_in_codemirror
@step(u'I go to the course updates page')
def go_to_updates(_step):
menu_css = 'li.nav-course-courseware'
updates_css = 'li.nav-course-courseware-updates'
world.css_click(menu_css)
world.css_click(updates_css)
@step(u'I add a new update with the text "([^"]*)"$')
def add_update(_step, text):
update_css = 'a.new-update-button'
world.css_click(update_css)
change_text(text)
@step(u'I should( not)? see the update "([^"]*)"$')
def check_update(_step, doesnt_see_update, text):
update_css = 'div.update-contents'
update = world.css_find(update_css)
if doesnt_see_update:
assert len(update) == 0 or not text in update.html
else:
assert text in update.html
@step(u'I modify the text to "([^"]*)"$')
def modify_update(_step, text):
button_css = 'div.post-preview a.edit-button'
world.css_click(button_css)
change_text(text)
@step(u'I delete the update$')
def click_button(_step):
button_css = 'div.post-preview a.delete-button'
world.css_click(button_css)
@step(u'I edit the date to "([^"]*)"$')
def change_date(_step, new_date):
button_css = 'div.post-preview a.edit-button'
world.css_click(button_css)
date_css = 'input.date'
date = world.css_find(date_css)
for i in range(len(date.value)):
date._element.send_keys(Keys.END, Keys.BACK_SPACE)
date._element.send_keys(new_date)
save_css = 'a.save-button'
world.css_click(save_css)
@step(u'I should see the date "([^"]*)"$')
def check_date(_step, date):
date_css = 'span.date-display'
date_html = world.css_find(date_css)
assert date == date_html.html
@step(u'I modify the handout to "([^"]*)"$')
def edit_handouts(_step, text):
edit_css = 'div.course-handouts > a.edit-button'
world.css_click(edit_css)
change_text(text)
@step(u'I see the handout "([^"]*)"$')
def check_handout(_step, handout):
handout_css = 'div.handouts-content'
handouts = world.css_find(handout_css)
assert handout in handouts.html
def change_text(text):
type_in_codemirror(0, text)
save_css = 'a.save-button'
world.css_click(save_css)
...@@ -10,6 +10,7 @@ from common import * ...@@ -10,6 +10,7 @@ from common import *
@step('There are no courses$') @step('There are no courses$')
def no_courses(step): def no_courses(step):
world.clear_courses() world.clear_courses()
create_studio_user()
@step('I click the New Course button$') @step('I click the New Course button$')
......
Feature: Static Pages
As a course author, I want to be able to add static pages
Scenario: Users can add static pages
Given I have opened a new course in Studio
And I go to the static pages page
When I add a new page
Then I should see a "Empty" static page
Scenario: Users can delete static pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
When I will confirm all alerts
And I "delete" the "Empty" page
Then I should not see a "Empty" static page
Scenario: Users can edit static pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
When I "edit" the "Empty" page
And I change the name to "New"
Then I should see a "New" static page
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from selenium.webdriver.common.keys import Keys
@step(u'I go to the static pages page')
def go_to_static(_step):
menu_css = 'li.nav-course-courseware'
static_css = 'li.nav-course-courseware-pages'
world.css_find(menu_css).click()
world.css_find(static_css).click()
@step(u'I add a new page')
def add_page(_step):
button_css = 'a.new-button'
world.css_find(button_css).click()
@step(u'I should( not)? see a "([^"]*)" static page$')
def see_page(_step, doesnt, page):
index = get_index(page)
if doesnt:
assert index == -1
else:
assert index != -1
@step(u'I "([^"]*)" the "([^"]*)" page$')
def click_edit_delete(_step, edit_delete, page):
button_css = 'a.%s-button' % edit_delete
index = get_index(page)
assert index != -1
world.css_find(button_css)[index].click()
@step(u'I change the name to "([^"]*)"$')
def change_name(_step, new_name):
settings_css = '#settings-mode'
world.css_find(settings_css).click()
input_css = 'input.setting-input'
name_input = world.css_find(input_css)
old_name = name_input.value
for count in range(len(old_name)):
name_input._element.send_keys(Keys.END, Keys.BACK_SPACE)
name_input._element.send_keys(new_name)
save_button = 'a.save-button'
world.css_find(save_button).click()
def get_index(name):
page_name_css = 'section[data-type="HTMLModule"]'
all_pages = world.css_find(page_name_css)
for i in range(len(all_pages)):
if all_pages[i].html == '\n {name}\n'.format(name=name):
return i
return -1
...@@ -50,7 +50,8 @@ def have_a_course_with_two_sections(step): ...@@ -50,7 +50,8 @@ def have_a_course_with_two_sections(step):
@step(u'I navigate to the course overview page$') @step(u'I navigate to the course overview page$')
def navigate_to_the_course_overview_page(step): def navigate_to_the_course_overview_page(step):
log_into_studio(is_staff=True) create_studio_user(is_staff=True)
log_into_studio()
course_locator = '.class-name' course_locator = '.class-name'
world.css_click(course_locator) world.css_click(course_locator)
......
Feature: Upload Files
As a course author, I want to be able to upload files for my students
Scenario: Users can upload files
Given I have opened a new course in Studio
And I go to the files and uploads page
When I upload the file "test"
Then I should see the file "test" was uploaded
And The url for the file "test" is valid
Scenario: Users can update files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the file "test"
And I upload the file "test"
Then I should see only one "test"
Scenario: Users can delete uploaded files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the file "test"
And I delete the file "test"
Then I should not see the file "test" was uploaded
Scenario: Users can download files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the file "test"
Then I can download the correct "test" file
Scenario: Users can download updated files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the file "test"
And I modify "test"
And I reload the page
And I upload the file "test"
Then I can download the correct "test" file
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from django.conf import settings
import requests
import string
import random
import os
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
HTTP_PREFIX = "http://localhost:8001"
@step(u'I go to the files and uploads page')
def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware'
uploads_css = 'li.nav-course-courseware-uploads'
world.css_find(menu_css).click()
world.css_find(uploads_css).click()
@step(u'I upload the file "([^"]*)"$')
def upload_file(_step, file_name):
upload_css = 'a.upload-button'
world.css_find(upload_css).click()
file_css = 'input.file-input'
upload = world.css_find(file_css)
#uploading the file itself
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
upload._element.send_keys(os.path.abspath(path))
close_css = 'a.close-button'
world.css_find(close_css).click()
@step(u'I should( not)? see the file "([^"]*)" was uploaded$')
def check_upload(_step, do_not_see_file, file_name):
index = get_index(file_name)
if do_not_see_file:
assert index == -1
else:
assert index != -1
@step(u'The url for the file "([^"]*)" is valid$')
def check_url(_step, file_name):
r = get_file(file_name)
assert r.status_code == 200
@step(u'I delete the file "([^"]*)"$')
def delete_file(_step, file_name):
index = get_index(file_name)
assert index != -1
delete_css = "a.remove-asset-button"
world.css_click(delete_css, index=index)
prompt_confirm_css = 'li.nav-item > a.action-primary'
world.css_click(prompt_confirm_css)
@step(u'I should see only one "([^"]*)"$')
def no_duplicate(_step, file_name):
names_css = 'td.name-col > a.filename'
all_names = world.css_find(names_css)
only_one = False
for i in range(len(all_names)):
if file_name == all_names[i].html:
only_one = not only_one
assert only_one
@step(u'I can download the correct "([^"]*)" file$')
def check_download(_step, file_name):
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
with open(os.path.abspath(path), 'r') as cur_file:
cur_text = cur_file.read()
r = get_file(file_name)
downloaded_text = r.text
assert cur_text == downloaded_text
@step(u'I modify "([^"]*)"$')
def modify_upload(_step, file_name):
new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10))
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
with open(os.path.abspath(path), 'w') as cur_file:
cur_file.write(new_text)
def get_index(file_name):
names_css = 'td.name-col > a.filename'
all_names = world.css_find(names_css)
for i in range(len(all_names)):
if file_name == all_names[i].html:
return i
return -1
def get_file(file_name):
index = get_index(file_name)
assert index != -1
url_css = 'input.embeddable-xml-input'
url = world.css_find(url_css)[index].value
return requests.get(HTTP_PREFIX + url)
...@@ -181,6 +181,6 @@ if SEGMENT_IO_KEY: ...@@ -181,6 +181,6 @@ if SEGMENT_IO_KEY:
##################################################################### #####################################################################
# Lastly, see if the developer has any local overrides. # Lastly, see if the developer has any local overrides.
try: try:
from .private import * from .private import * # pylint: disable=F0401
except ImportError: except ImportError:
pass pass
...@@ -100,11 +100,10 @@ describe "CMS.Views.SystemFeedback click events", -> ...@@ -100,11 +100,10 @@ describe "CMS.Views.SystemFeedback click events", ->
text: "Save", text: "Save",
class: "save-button", class: "save-button",
click: @primaryClickSpy click: @primaryClickSpy
secondary: [{ secondary:
text: "Revert", text: "Revert",
class: "cancel-button", class: "cancel-button",
click: @secondaryClickSpy click: @secondaryClickSpy
}]
) )
@view.show() @view.show()
...@@ -124,6 +123,46 @@ describe "CMS.Views.SystemFeedback click events", -> ...@@ -124,6 +123,46 @@ describe "CMS.Views.SystemFeedback click events", ->
it "should apply class to secondary action", -> it "should apply class to secondary action", ->
expect(@view.$(".action-secondary")).toHaveClass("cancel-button") expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
describe "CMS.Views.SystemFeedback multiple secondary actions", ->
beforeEach ->
@secondarySpyOne = jasmine.createSpy('secondarySpyOne')
@secondarySpyTwo = jasmine.createSpy('secondarySpyTwo')
@view = new CMS.Views.Notification.Warning(
title: "No Primary",
message: "Pick a secondary action",
actions:
secondary: [
{
text: "Option One"
class: "option-one"
click: @secondarySpyOne
}, {
text: "Option Two"
class: "option-two"
click: @secondarySpyTwo
}
]
)
@view.show()
it "should render both", ->
expect(@view.el).toContain(".action-secondary.option-one")
expect(@view.el).toContain(".action-secondary.option-two")
expect(@view.el).not.toContain(".action-secondary.option-one.option-two")
expect(@view.$(".action-secondary.option-one")).toContainText("Option One")
expect(@view.$(".action-secondary.option-two")).toContainText("Option Two")
it "should differentiate clicks (1)", ->
@view.$(".option-one").click()
expect(@secondarySpyOne).toHaveBeenCalled()
expect(@secondarySpyTwo).not.toHaveBeenCalled()
it "should differentiate clicks (2)", ->
@view.$(".option-two").click()
expect(@secondarySpyOne).not.toHaveBeenCalled()
expect(@secondarySpyTwo).toHaveBeenCalled()
describe "CMS.Views.Notification minShown and maxShown", -> describe "CMS.Views.Notification minShown and maxShown", ->
beforeEach -> beforeEach ->
@showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show') @showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show')
......
...@@ -25,7 +25,6 @@ $(document).ready(function() { ...@@ -25,7 +25,6 @@ $(document).ready(function() {
$newComponentTemplatePickers = $('.new-component-templates'); $newComponentTemplatePickers = $('.new-component-templates');
$newComponentButton = $('.new-component-button'); $newComponentButton = $('.new-component-button');
$spinner = $('<span class="spinner-in-field-icon"></span>'); $spinner = $('<span class="spinner-in-field-icon"></span>');
$body.bind('keyup', onKeyUp);
$('.expand-collapse-icon').bind('click', toggleSubmodules); $('.expand-collapse-icon').bind('click', toggleSubmodules);
$('.visibility-options').bind('change', setVisibility); $('.visibility-options').bind('change', setVisibility);
...@@ -413,12 +412,6 @@ function hideModal(e) { ...@@ -413,12 +412,6 @@ function hideModal(e) {
} }
} }
function onKeyUp(e) {
if (e.which == 87) {
$body.toggleClass('show-wip hide-wip');
}
}
function toggleSock(e) { function toggleSock(e) {
e.preventDefault(); e.preventDefault();
......
...@@ -49,6 +49,11 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ ...@@ -49,6 +49,11 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
} }
this.template = _.template(tpl); this.template = _.template(tpl);
this.setElement($("#page-"+this.options.type)); this.setElement($("#page-"+this.options.type));
// handle single "secondary" action
if (this.options.actions && this.options.actions.secondary &&
!_.isArray(this.options.actions.secondary)) {
this.options.actions.secondary = [this.options.actions.secondary];
}
return this; return this;
}, },
// public API: show() and hide() // public API: show() and hide()
......
...@@ -20,9 +20,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -20,9 +20,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
self.render(); self.render();
} }
); );
// because these are outside of this.$el, they can't be in the event hash
$('.save-button').on('click', this, this.saveView);
$('.cancel-button').on('click', this, this.revertView);
this.listenTo(this.model, 'invalid', this.handleValidationError); this.listenTo(this.model, 'invalid', this.handleValidationError);
}, },
render: function() { render: function() {
...@@ -45,7 +42,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -45,7 +42,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var policyValues = listEle$.find('.json'); var policyValues = listEle$.find('.json');
_.each(policyValues, this.attachJSONEditor, this); _.each(policyValues, this.attachJSONEditor, this);
this.showMessage();
return this; return this;
}, },
attachJSONEditor : function (textarea) { attachJSONEditor : function (textarea) {
...@@ -61,7 +57,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -61,7 +57,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mode: "application/json", lineNumbers: false, lineWrapping: false, mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) { onChange: function(instance, changeobj) {
// this event's being called even when there's no change :-( // this event's being called even when there's no change :-(
if (instance.getValue() !== oldValue) self.showSaveCancelButtons(); if (instance.getValue() !== oldValue && !self.notificationBarShowing) {
self.showNotificationBar();
}
}, },
onFocus : function(mirror) { onFocus : function(mirror) {
$(textarea).parent().children('label').addClass("is-focused"); $(textarea).parent().children('label').addClass("is-focused");
...@@ -99,59 +97,65 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -99,59 +97,65 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
} }
}); });
}, },
showMessage: function (type) { showNotificationBar: function() {
$(".wrapper-alert").removeClass("is-shown"); var self = this;
if (type) { var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.")
if (type === this.error_saving) { var confirm = new CMS.Views.Notification.Warning({
$(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false'); title: gettext("You've Made Some Changes"),
} message: message,
else if (type === this.successful_changes) { actions: {
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); primary: {
this.hideSaveCancelButtons(); "text": gettext("Save Changes"),
} "class": "action-save",
} "click": function() {
else { self.saveView();
// This is the case of the page first rendering, or when Cancel is pressed. confirm.hide();
this.hideSaveCancelButtons(); self.notificationBarShowing = false;
} }
}, },
showSaveCancelButtons: function(event) { secondary: [{
if (!this.notificationBarShowing) { "text": gettext("Cancel"),
this.$el.find(".message-status").removeClass("is-shown"); "class": "action-cancel",
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false'); "click": function() {
this.notificationBarShowing = true; self.revertView();
} confirm.hide();
}, self.notificationBarShowing = false;
hideSaveCancelButtons: function() { }
if (this.notificationBarShowing) { }]
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); }});
this.notificationBarShowing = false; this.notificationBarShowing = true;
confirm.show();
if(this.saved) {
this.saved.hide();
} }
}, },
saveView : function(event) { saveView : function() {
window.CmsUtils.smoothScrollTop(event);
// TODO one last verification scan: // TODO one last verification scan:
// call validateKey on each to ensure proper format // call validateKey on each to ensure proper format
// check for dupes // check for dupes
var self = event.data; var self = this;
self.model.save({}, this.model.save({},
{ {
success : function() { success : function() {
self.render(); self.render();
self.showMessage(self.successful_changes); var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.");
self.saved = new CMS.Views.Alert.Confirmation({
title: gettext("Your policy changes have been saved."),
message: message,
closeIcon: false
});
self.saved.show();
analytics.track('Saved Advanced Settings', { analytics.track('Saved Advanced Settings', {
'course': course_location_analytics 'course': course_location_analytics
}); });
} }
}); });
}, },
revertView : function(event) { revertView : function() {
event.preventDefault(); var self = this;
var self = event.data; this.model.deleteKeys = [];
self.model.deleteKeys = []; this.model.clear({silent : true});
self.model.clear({silent : true}); this.model.fetch({
self.model.fetch({
success : function() { self.render(); }, success : function() { self.render(); },
reset: true reset: true
}); });
......
...@@ -61,8 +61,6 @@ ...@@ -61,8 +61,6 @@
<div class="wrapper wrapper-view"> <div class="wrapper wrapper-view">
<%include file="widgets/header.html" /> <%include file="widgets/header.html" />
## remove this block after advanced settings notification is rewritten
<%block name="view_alerts"></%block>
<div id="page-alert"></div> <div id="page-alert"></div>
<%block name="content"></%block> <%block name="content"></%block>
...@@ -74,13 +72,9 @@ ...@@ -74,13 +72,9 @@
<%include file="widgets/footer.html" /> <%include file="widgets/footer.html" />
<%include file="widgets/tender.html" /> <%include file="widgets/tender.html" />
## remove this block after advanced settings notification is rewritten
<%block name="view_notifications"></%block>
<div id="page-notification"></div> <div id="page-notification"></div>
</div> </div>
## remove this block after advanced settings notification is rewritten
<%block name="view_prompts"></%block>
<div id="page-prompt"></div> <div id="page-prompt"></div>
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
......
...@@ -104,60 +104,3 @@ editor.render(); ...@@ -104,60 +104,3 @@ editor.render();
</section> </section>
</div> </div>
</%block> </%block>
<%block name="view_notifications">
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description">
<div class="notification warning has-actions">
<i class="icon-warning-sign"></i>
<div class="copy">
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item">
<a href="" class="action-primary save-button">Save Changes</a>
</li>
<li class="nav-item">
<a href="" class="action-secondary cancel-button">Cancel</a>
</li>
</ul>
</nav>
</div>
</div>
</%block>
<%block name="view_alerts">
<!-- alert: save confirmed with close -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
<div class="alert confirmation">
<i class="icon-ok"></i>
<div class="copy">
<h2 class="title title-3">Your policy changes have been saved.</h2>
<p>Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.</p>
</div>
<a href="" rel="view" class="action action-alert-close">
<i class="icon-remove-sign"></i>
<span class="label">close alert</span>
</a>
</div>
</div>
<!-- alert: error -->
<div class="wrapper wrapper-alert wrapper-alert-error" role="status">
<div class="alert error">
<i class="icon-warning-sign"></i>
<div class="copy">
<h2 class="title title-3">There was an error saving your information</h2>
<p>Please see the error below and correct it to ensure there are no problems in rendering your course.</p>
</div>
</div>
</div>
</%block>
...@@ -3,7 +3,7 @@ import json ...@@ -3,7 +3,7 @@ import json
import logging import logging
import random import random
import re import re
import string import string # pylint: disable=W0402
import fnmatch import fnmatch
from textwrap import dedent from textwrap import dedent
......
from django.conf.urls import * from django.conf.urls import url, patterns
urlpatterns = patterns('', # nopep8 urlpatterns = patterns('', # nopep8
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'), url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
django admin pages for courseware model django admin pages for courseware model
''' '''
from student.models import * from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
from student.models import CourseEnrollment, Registration, PendingNameChange
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import User
admin.site.register(UserProfile) admin.site.register(UserProfile)
......
...@@ -37,7 +37,6 @@ rate -- messages per second ...@@ -37,7 +37,6 @@ rate -- messages per second
self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n') self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n')
def handle(self, *args, **options): def handle(self, *args, **options):
global log_file
(user_file, message_base, logfilename, ratestr) = args (user_file, message_base, logfilename, ratestr) = args
users = [u.strip() for u in open(user_file).readlines()] users = [u.strip() for u in open(user_file).readlines()]
......
...@@ -55,11 +55,15 @@ class ReactivationEmailTests(EmailTestMixin, TestCase): ...@@ -55,11 +55,15 @@ class ReactivationEmailTests(EmailTestMixin, TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory.create() self.user = UserFactory.create()
self.unregisteredUser = UserFactory.create()
self.registration = RegistrationFactory.create(user=self.user) self.registration = RegistrationFactory.create(user=self.user)
def reactivation_email(self): def reactivation_email(self, user):
"""Send the reactivation email, and return the response as json data""" """
return json.loads(reactivation_email_for_user(self.user).content) Send the reactivation email to the specified user,
and return the response as json data.
"""
return json.loads(reactivation_email_for_user(user).content)
def assertReactivateEmailSent(self, email_user): def assertReactivateEmailSent(self, email_user):
"""Assert that the correct reactivation email has been sent""" """Assert that the correct reactivation email has been sent"""
...@@ -78,13 +82,22 @@ class ReactivationEmailTests(EmailTestMixin, TestCase): ...@@ -78,13 +82,22 @@ class ReactivationEmailTests(EmailTestMixin, TestCase):
def test_reactivation_email_failure(self, email_user): def test_reactivation_email_failure(self, email_user):
self.user.email_user.side_effect = Exception self.user.email_user.side_effect = Exception
response_data = self.reactivation_email() response_data = self.reactivation_email(self.user)
self.assertReactivateEmailSent(email_user) self.assertReactivateEmailSent(email_user)
self.assertFalse(response_data['success']) self.assertFalse(response_data['success'])
def test_reactivation_for_unregistered_user(self, email_user):
"""
Test that trying to send a reactivation email to an unregistered
user fails without throwing a 500 error.
"""
response_data = self.reactivation_email(self.unregisteredUser)
self.assertFalse(response_data['success'])
def test_reactivation_email_success(self, email_user): def test_reactivation_email_success(self, email_user):
response_data = self.reactivation_email() response_data = self.reactivation_email(self.user)
self.assertReactivateEmailSent(email_user) self.assertReactivateEmailSent(email_user)
self.assertTrue(response_data['success']) self.assertTrue(response_data['success'])
...@@ -150,7 +163,7 @@ class EmailChangeRequestTests(TestCase): ...@@ -150,7 +163,7 @@ class EmailChangeRequestTests(TestCase):
self.check_duplicate_email(self.new_email) self.check_duplicate_email(self.new_email)
def test_capitalized_duplicate_email(self): def test_capitalized_duplicate_email(self):
raise SkipTest("We currently don't check for emails in a case insensitive way, but we should") """Test that we check for email addresses in a case insensitive way"""
UserFactory.create(email=self.new_email) UserFactory.create(email=self.new_email)
self.check_duplicate_email(self.new_email.capitalize()) self.check_duplicate_email(self.new_email.capitalize())
......
...@@ -4,7 +4,7 @@ import json ...@@ -4,7 +4,7 @@ import json
import logging import logging
import random import random
import re import re
import string import string # pylint: disable=W0402
import urllib import urllib
import uuid import uuid
import time import time
...@@ -176,7 +176,7 @@ def _cert_info(user, course, cert_status): ...@@ -176,7 +176,7 @@ def _cert_info(user, course, cert_status):
CertificateStatuses.downloadable: 'ready', CertificateStatuses.downloadable: 'ready',
CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.notpassing: 'notpassing',
CertificateStatuses.restricted: 'restricted', CertificateStatuses.restricted: 'restricted',
} }
status = template_state.get(cert_status['status'], default_status) status = template_state.get(cert_status['status'], default_status)
...@@ -185,10 +185,10 @@ def _cert_info(user, course, cert_status): ...@@ -185,10 +185,10 @@ def _cert_info(user, course, cert_status):
'show_disabled_download_button': status == 'generating', } 'show_disabled_download_button': status == 'generating', }
if (status in ('generating', 'ready', 'notpassing', 'restricted') and if (status in ('generating', 'ready', 'notpassing', 'restricted') and
course.end_of_course_survey_url is not None): course.end_of_course_survey_url is not None):
d.update({ d.update({
'show_survey_button': True, 'show_survey_button': True,
'survey_url': process_survey_link(course.end_of_course_survey_url, user)}) 'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
else: else:
d['show_survey_button'] = False d['show_survey_button'] = False
...@@ -913,8 +913,8 @@ def get_random_post_override(): ...@@ -913,8 +913,8 @@ def get_random_post_override():
'password': id_generator(), 'password': id_generator(),
'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " + 'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " +
id_generator(size=7, chars=string.ascii_lowercase)), id_generator(size=7, chars=string.ascii_lowercase)),
'honor_code': u'true', 'honor_code': u'true',
'terms_of_service': u'true', } 'terms_of_service': u'true', }
def create_random_account(create_account_function): def create_random_account(create_account_function):
...@@ -985,21 +985,12 @@ def password_reset(request): ...@@ -985,21 +985,12 @@ def password_reset(request):
'error': 'Invalid e-mail'})) 'error': 'Invalid e-mail'}))
@ensure_csrf_cookie def reactivation_email_for_user(user):
def reactivation_email(request):
''' Send an e-mail to reactivate a deactivated account, or to
resend an activation e-mail. Untested. '''
email = request.POST['email']
try: try:
user = User.objects.get(email='email') reg = Registration.objects.get(user=user)
except User.DoesNotExist: except Registration.DoesNotExist:
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'No inactive user with this e-mail exists'})) 'error': 'No inactive user with this e-mail exists'}))
return reactivation_email_for_user(user)
def reactivation_email_for_user(user):
reg = Registration.objects.get(user=user)
d = {'name': user.profile.name, d = {'name': user.profile.name,
'key': reg.activation_key} 'key': reg.activation_key}
......
...@@ -10,10 +10,9 @@ from django.contrib.auth import authenticate, login ...@@ -10,10 +10,9 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from bs4 import BeautifulSoup
import os.path
from urllib import quote_plus from urllib import quote_plus
...@@ -75,51 +74,6 @@ def register_by_course_id(course_id, is_staff=False): ...@@ -75,51 +74,6 @@ def register_by_course_id(course_id, is_staff=False):
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
@world.absorb
def save_the_course_content(path='/tmp'):
html = world.browser.html.encode('ascii', 'ignore')
soup = BeautifulSoup(html)
# get rid of the header, we only want to compare the body
soup.head.decompose()
# for now, remove the data-id attributes, because they are
# causing mismatches between cms-master and master
for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
del item['data-id']
# we also need to remove them from unrendered problems,
# where they are contained in the text of divs instead of
# in attributes of tags
# Be careful of whether or not it was the last attribute
# and needs a trailing space
for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
for item in soup.find_all(text=re.compile(' data-id=".*?"')):
s = unicode(item.string)
item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
# prettify the html so it will compare better, with
# each HTML tag on its own line
output = soup.prettify()
# use string slicing to grab everything after 'courseware/' in the URL
u = world.browser.url
section_url = u[u.find('courseware/') + 11:]
if not os.path.exists(path):
os.makedirs(path)
filename = '%s.html' % (quote_plus(section_url))
f = open('%s/%s' % (path, filename), 'w')
f.write(output)
f.close
@world.absorb @world.absorb
def clear_courses(): def clear_courses():
# Flush and initialize the module store # Flush and initialize the module store
...@@ -129,6 +83,6 @@ def clear_courses(): ...@@ -129,6 +83,6 @@ def clear_courses():
# (though it shouldn't), do this manually # (though it shouldn't), do this manually
# from the bash shell to drop it: # from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()" # $ mongo test_xmodule --eval "db.dropDatabase()"
_MODULESTORES = {}
modulestore().collection.drop() modulestore().collection.drop()
update_templates(modulestore('direct')) update_templates(modulestore('direct'))
contentstore().fs_files.drop()
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
django admin pages for courseware model django admin pages for courseware model
''' '''
from track.models import * from track.models import TrackingLog
from django.contrib import admin from django.contrib import admin
admin.site.register(TrackingLog) admin.site.register(TrackingLog)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# Provides sympy representation. # Provides sympy representation.
import os import os
import string import string # pylint: disable=W0402
import re import re
import logging import logging
import operator import operator
......
...@@ -18,8 +18,6 @@ def load_function(path): ...@@ -18,8 +18,6 @@ def load_function(path):
def contentstore(name='default'): def contentstore(name='default'):
global _CONTENTSTORE
if name not in _CONTENTSTORE: if name not in _CONTENTSTORE:
class_ = load_function(settings.CONTENTSTORE['ENGINE']) class_ = load_function(settings.CONTENTSTORE['ENGINE'])
options = {} options = {}
......
...@@ -26,8 +26,6 @@ def load_function(path): ...@@ -26,8 +26,6 @@ def load_function(path):
def modulestore(name='default'): def modulestore(name='default'):
global _MODULESTORES
if name not in _MODULESTORES: if name not in _MODULESTORES:
class_ = load_function(settings.MODULESTORE[name]['ENGINE']) class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
......
R2FUIGM88K
\ No newline at end of file
...@@ -58,21 +58,24 @@ In the discussion service, notifications are handled asynchronously using a thir ...@@ -58,21 +58,24 @@ In the discussion service, notifications are handled asynchronously using a thir
bundle exec rake jobs:work bundle exec rake jobs:work
## Initialize roles and permissions ## From the edx-platform django app, initialize roles and permissions
To fully test the discussion forum, you might want to act as a moderator or an administrator. Currently, moderators can manage everything in the forum, and administrator can manage everything plus assigning and revoking moderator status of other users. To fully test the discussion forum, you might want to act as a moderator or an administrator. Currently, moderators can manage everything in the forum, and administrator can manage everything plus assigning and revoking moderator status of other users.
First make sure that the database is up-to-date: First make sure that the database is up-to-date:
rake django-admin[syncdb] rake resetdb
rake django-admin[migrate]
If you have created users in the edx-platform django apps when the comment service was not running, you will need to one-way sync the users into the comment service back end database:
rake django-admin[sync_user_info]
For convenience, add the following environment variables to the terminal (assuming that you're using configuration set lms.envs.dev): For convenience, add the following environment variables to the terminal (assuming that you're using configuration set lms.envs.dev):
export DJANGO_SETTINGS_MODULE=lms.envs.dev export DJANGO_SETTINGS_MODULE=lms.envs.dev
export PYTHONPATH=. export PYTHONPATH=.
Now initialzie roles and permissions, providing a course id eg.: Now initialize roles and permissions, providing a course id. See the example below. Note that you do not need to do this for Studio-created courses, as the Studio application does this for you.
django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall" django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
django admin pages for courseware model django admin pages for courseware model
''' '''
from courseware.models import * from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import User from django.contrib.auth.models import User
......
# Here are all the courses for Fall 2012
# MITx/3.091x/2012_Fall
# MITx/6.002x/2012_Fall
# MITx/6.00x/2012_Fall
# HarvardX/CS50x/2012 (we will not be testing this, as it is anomolistic)
# HarvardX/PH207x/2012_Fall
# BerkeleyX/CS169.1x/2012_Fall
# BerkeleyX/CS169.2x/2012_Fall
# BerkeleyX/CS184.1x/2012_Fall
#You can load the courses into your data directory with these cmds:
# git clone https://github.com/MITx/3.091x.git
# git clone https://github.com/MITx/6.00x.git
# git clone https://github.com/MITx/content-mit-6002x.git
# git clone https://github.com/MITx/content-mit-6002x.git
# git clone https://github.com/MITx/content-harvard-id270x.git
# git clone https://github.com/MITx/content-berkeley-cs169x.git
# git clone https://github.com/MITx/content-berkeley-cs169.2x.git
# git clone https://github.com/MITx/content-berkeley-cs184x.git
Feature: There are courses on the homepage
In order to compared rendered content to the database
As an acceptance test
I want to count all the chapters, sections, and tabs for each course
# Commenting these all out for now because they don't always run,
# they have too many prerequesites, e.g. the course exists, and
# is within the start and end dates, etc.
# Scenario: Navigate through course MITx/3.091x/2012_Fall
# Given I am registered for course "MITx/3.091x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course MITx/6.002x/2012_Fall
# Given I am registered for course "MITx/6.002x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course MITx/6.00x/2012_Fall
# Given I am registered for course "MITx/6.00x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course HarvardX/PH207x/2012_Fall
# Given I am registered for course "HarvardX/PH207x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall
# Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall"
# And I log in
# Then I verify all the content of each course
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
# And I log in
# Then I verify all the content of each course
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from re import sub
from nose.tools import assert_equals
from xmodule.modulestore.django import modulestore
from common import *
from logging import getLogger
logger = getLogger(__name__)
def check_for_errors():
e = world.browser.find_by_css('.outside-app')
if len(e) > 0:
assert False, 'there was a server error at %s' % (world.browser.url)
else:
assert True
@step(u'I verify all the content of each course')
def i_verify_all_the_content_of_each_course(step):
all_possible_courses = get_courses()
logger.debug('Courses found:')
for c in all_possible_courses:
logger.debug(c.id)
ids = [c.id for c in all_possible_courses]
# Get a list of all the registered courses
registered_courses = world.browser.find_by_css('article.my-course')
if len(all_possible_courses) < len(registered_courses):
assert False, "user is registered for more courses than are uniquely posssible"
else:
pass
for test_course in registered_courses:
test_course.css_click('a')
check_for_errors()
# Get the course. E.g. 'MITx/6.002x/2012_Fall'
current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url))
validate_course(current_course, ids)
world.click_link('Courseware')
assert world.is_css_present('accordion')
check_for_errors()
browse_course(current_course)
# clicking the user link gets you back to the user's home page
world.css_click('.user-link')
check_for_errors()
def browse_course(course_id):
## count chapters from xml and page and compare
chapters = get_courseware_with_tabs(course_id)
num_chapters = len(chapters)
rendered_chapters = world.browser.find_by_css('#accordion > nav > div')
num_rendered_chapters = len(rendered_chapters)
msg = '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id)
#logger.debug(msg)
assert num_chapters == num_rendered_chapters, msg
chapter_it = 0
## Iterate the chapters
while chapter_it < num_chapters:
## click into a chapter
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click()
## look for the "there was a server error" div
check_for_errors()
## count sections from xml and page and compare
sections = chapters[chapter_it]['sections']
num_sections = len(sections)
rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')
num_rendered_sections = len(rendered_sections)
msg = ('%d sections expected, %d sections found on page, %s - %d - %s' %
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
#logger.debug(msg)
assert num_sections == num_rendered_sections, msg
section_it = 0
## Iterate the sections
while section_it < num_sections:
## click on a section
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
## sometimes the course-content takes a long time to load
assert world.is_css_present('.course-content')
## look for server error div
check_for_errors()
## count tabs from xml and page and compare
## count the number of tabs. If number of tabs is 0, there won't be anything rendered
## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length
num_tabs = sections[section_it]['clickable_tab_count']
if num_tabs != 0:
rendered_tabs = world.browser.find_by_css('ol#sequence-list > li')
num_rendered_tabs = len(rendered_tabs)
else:
rendered_tabs = 0
num_rendered_tabs = 0
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
#logger.debug(msg)
# Save the HTML to a file for later comparison
world.save_the_course_content('/tmp/%s' % course_id)
assert num_tabs == num_rendered_tabs, msg
tabs = sections[section_it]['tabs']
tab_it = 0
## Iterate the tabs
while tab_it < num_tabs:
rendered_tabs[tab_it].find_by_tag('a').click()
## do something with the tab sections[section_it]
# e = world.browser.find_by_css('section.course-content section')
# process_section(e)
tab_children = tabs[tab_it]['children_count']
tab_class = tabs[tab_it]['class']
if tab_children != 0:
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
num_rendered_items = len(rendered_items)
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
#logger.debug(msg)
assert tab_children == num_rendered_items, msg
tab_it += 1
section_it += 1
chapter_it += 1
def validate_course(current_course, ids):
try:
ids.index(current_course)
except:
assert False, "invalid course id %s" % current_course
...@@ -2,7 +2,7 @@ from django.conf import settings ...@@ -2,7 +2,7 @@ from django.conf import settings
from .mustache_helpers import mustache_helpers from .mustache_helpers import mustache_helpers
from functools import partial from functools import partial
from .utils import * from .utils import extend_content, merge_dict, render_mustache
import django_comment_client.settings as cc_settings import django_comment_client.settings as cc_settings
import pystache_custom as pystache import pystache_custom as pystache
......
import string import string # pylint: disable=W0402
import random import random
from django.contrib.auth.models import User from django.contrib.auth.models import User
......
...@@ -73,21 +73,17 @@ def get_discussion_id_map(course): ...@@ -73,21 +73,17 @@ def get_discussion_id_map(course):
""" """
return a dict of the form {category: modules} return a dict of the form {category: modules}
""" """
global _DISCUSSIONINFO
initialize_discussion_info(course) initialize_discussion_info(course)
return _DISCUSSIONINFO[course.id]['id_map'] return _DISCUSSIONINFO[course.id]['id_map']
def get_discussion_title(course, discussion_id): def get_discussion_title(course, discussion_id):
global _DISCUSSIONINFO
initialize_discussion_info(course) initialize_discussion_info(course)
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)') title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
return title return title
def get_discussion_category_map(course): def get_discussion_category_map(course):
global _DISCUSSIONINFO
initialize_discussion_info(course) initialize_discussion_info(course)
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map']) return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
...@@ -141,8 +137,6 @@ def sort_map_entries(category_map): ...@@ -141,8 +137,6 @@ def sort_map_entries(category_map):
def initialize_discussion_info(course): def initialize_discussion_info(course):
global _DISCUSSIONINFO
course_id = course.id course_id = course.id
discussion_id_map = {} discussion_id_map = {}
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# django management command: dump grades to csv files # django management command: dump grades to csv files
# for use by batch processes # for use by batch processes
from instructor.offline_gradecalc import * from instructor.offline_gradecalc import offline_grade_calculation
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
......
...@@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore ...@@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore
USER_COUNT = 11 USER_COUNT = 11
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestGradebook(ModuleStoreTestCase): class TestGradebook(ModuleStoreTestCase):
grading_policy = None grading_policy = None
...@@ -41,10 +42,7 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -41,10 +42,7 @@ class TestGradebook(ModuleStoreTestCase):
metadata={'graded': True, 'format': 'Homework'} metadata={'graded': True, 'format': 'Homework'}
) )
self.users = [ self.users = [UserFactory() for _ in xrange(USER_COUNT)]
UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i)
for i in xrange(USER_COUNT)
]
for user in self.users: for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id) CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
...@@ -72,10 +70,11 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -72,10 +70,11 @@ class TestGradebook(ModuleStoreTestCase):
def test_response_code(self): def test_response_code(self):
self.assertEquals(self.response.status_code, 200) self.assertEquals(self.response.status_code, 200)
class TestDefaultGradingPolicy(TestGradebook): class TestDefaultGradingPolicy(TestGradebook):
def test_all_users_listed(self): def test_all_users_listed(self):
for user in self.users: for user in self.users:
self.assertIn(user.username, self.response.content) self.assertIn(user.username, unicode(self.response.content, 'utf-8'))
def test_default_policy(self): def test_default_policy(self):
# Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6] # Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6]
...@@ -92,6 +91,7 @@ class TestDefaultGradingPolicy(TestGradebook): ...@@ -92,6 +91,7 @@ class TestDefaultGradingPolicy(TestGradebook):
# One use at the top of the page [1] # One use at the top of the page [1]
self.assertEquals(293, self.response.content.count('grade_None')) self.assertEquals(293, self.response.content.count('grade_None'))
class TestLetterCutoffPolicy(TestGradebook): class TestLetterCutoffPolicy(TestGradebook):
grading_policy = { grading_policy = {
"GRADER": [ "GRADER": [
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import os import os
import sys import sys
import string import string # pylint: disable=W0402
import datetime import datetime
from getpass import getpass from getpass import getpass
import json import json
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
django admin pages for courseware model django admin pages for courseware model
''' '''
from psychometrics.models import * from psychometrics.models import PsychometricData
from django.contrib import admin from django.contrib import admin
admin.site.register(PsychometricData) admin.site.register(PsychometricData)
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
import json import json
from courseware.models import * from courseware.models import StudentModule
from track.models import * from track.models import TrackingLog
from psychometrics.models import * from psychometrics.models import PsychometricData
from xmodule.modulestore import Location from xmodule.modulestore import Location
from django.conf import settings from django.conf import settings
......
...@@ -14,7 +14,8 @@ from scipy.optimize import curve_fit ...@@ -14,7 +14,8 @@ from scipy.optimize import curve_fit
from django.conf import settings from django.conf import settings
from django.db.models import Sum, Max from django.db.models import Sum, Max
from psychometrics.models import * from psychometrics.models import PsychometricData
from courseware.models import StudentModule
from pytz import UTC from pytz import UTC
log = logging.getLogger("mitx.psychometrics") log = logging.getLogger("mitx.psychometrics")
...@@ -303,7 +304,7 @@ def generate_plots_for_problem(problem): ...@@ -303,7 +304,7 @@ def generate_plots_for_problem(problem):
def make_psychometrics_data_update_handler(course_id, user, module_state_key): def make_psychometrics_data_update_handler(course_id, user, module_state_key):
""" """
Construct and return a procedure which may be called to update Construct and return a procedure which may be called to update
the PsychometricsData instance for the given StudentModule instance. the PsychometricData instance for the given StudentModule instance.
""" """
sm, status = StudentModule.objects.get_or_create( sm, status = StudentModule.objects.get_or_create(
course_id=course_id, course_id=course_id,
......
...@@ -103,6 +103,13 @@ MITX_FEATURES = { ...@@ -103,6 +103,13 @@ MITX_FEATURES = {
# analytics experiments # analytics experiments
'ENABLE_INSTRUCTOR_ANALYTICS': False, 'ENABLE_INSTRUCTOR_ANALYTICS': False,
# enable analytics server.
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
# LMS OPERATION. See analytics.py for details about what
# this does.
'RUN_AS_ANALYTICS_SERVER_ENABLED' : False,
# Flip to True when the YouTube iframe API breaks (again) # Flip to True when the YouTube iframe API breaks (again)
'USE_YOUTUBE_OBJECT_API': False, 'USE_YOUTUBE_OBJECT_API': False,
......
...@@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY: ...@@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY:
##################################################################### #####################################################################
# Lastly, see if the developer has any local overrides. # Lastly, see if the developer has any local overrides.
try: try:
from .private import * from .private import * # pylint: disable=F0401
except ImportError: except ImportError:
pass pass
from .utils import * from .utils import CommentClientError, perform_request
from .thread import Thread from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread
import models import models
import settings import settings
......
...@@ -5,7 +5,7 @@ from .thread import Thread ...@@ -5,7 +5,7 @@ from .thread import Thread
from .user import User from .user import User
from .commentable import Commentable from .commentable import Commentable
from .utils import * from .utils import perform_request
import settings import settings
......
from .utils import *
import models import models
import settings import settings
......
def delete_threads(commentable_id, *args, **kwargs):
return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def search_similar_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_similar_threads(), attributes, *args, **kwargs)
def search_recent_active_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_recent_active_threads(), attributes, *args, **kwargs)
def search_trending_tags(course_id, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_trending_tags(), attributes, *args, **kwargs)
def create_user(attributes, *args, **kwargs):
return _perform_request('post', _url_for_users(), attributes, *args, **kwargs)
def update_user(user_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_user(user_id), attributes, *args, **kwargs)
def get_threads_tags(*args, **kwargs):
return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
def tags_autocomplete(value, *args, **kwargs):
return _perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs)
def create_thread(commentable_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs)
def get_thread(thread_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs)
def update_thread(thread_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs)
def create_comment(thread_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs)
def delete_thread(thread_id, *args, **kwargs):
return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs)
def get_comment(comment_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs)
def update_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs)
def create_sub_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs)
def delete_comment(comment_id, *args, **kwargs):
return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs)
def vote_for_comment(comment_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_comment(comment_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_comment(comment_id), {'user_id': user_id}, *args, **kwargs)
def vote_for_thread(thread_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_thread(thread_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_thread(thread_id), {'user_id': user_id}, *args, **kwargs)
def get_notifications(user_id, *args, **kwargs):
return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs)
def get_user_info(user_id, complete=True, *args, **kwargs):
return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs)
def subscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def subscribe_user(user_id, followed_user_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
follow = subscribe_user
def subscribe_thread(user_id, thread_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def subscribe_commentable(user_id, commentable_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def unsubscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def unsubscribe_user(user_id, followed_user_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
unfollow = unsubscribe_user
def unsubscribe_thread(user_id, thread_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def _perform_request(method, url, data_or_params=None, *args, **kwargs):
if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params)
else:
response = requests.request(method, url, params=data_or_params)
if 200 < response.status_code < 500:
raise CommentClientError(response.text)
elif response.status_code == 500:
raise CommentClientUnknownError(response.text)
else:
if kwargs.get("raw", False):
return response.text
else:
return json.loads(response.text)
def _url_for_threads(commentable_id):
return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id)
def _url_for_thread(thread_id):
return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_thread_comments(thread_id):
return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_comment(comment_id):
return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_thread(thread_id):
return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_notifications(user_id):
return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id)
def _url_for_subscription(user_id):
return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id)
def _url_for_user(user_id):
return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id)
def _url_for_search_threads():
return "{prefix}/search/threads".format(prefix=PREFIX)
def _url_for_search_similar_threads():
return "{prefix}/search/threads/more_like_this".format(prefix=PREFIX)
def _url_for_search_recent_active_threads():
return "{prefix}/search/threads/recent_active".format(prefix=PREFIX)
def _url_for_search_trending_tags():
return "{prefix}/search/tags/trending".format(prefix=PREFIX)
def _url_for_threads_tags():
return "{prefix}/threads/tags".format(prefix=PREFIX)
def _url_for_threads_tags_autocomplete():
return "{prefix}/threads/tags/autocomplete".format(prefix=PREFIX)
def _url_for_users():
return "{prefix}/users".format(prefix=PREFIX)
from .utils import * from .utils import merge_dict, strip_blank, strip_none, extract, perform_request
from .utils import CommentClientError
import models import models
import settings import settings
......
from .utils import * from .utils import merge_dict, perform_request, CommentClientError
import models import models
import settings import settings
......
...@@ -46,6 +46,13 @@ form { ...@@ -46,6 +46,13 @@ form {
} }
} }
form.choicegroup {
label {
clear: both;
float: left;
}
}
textarea, textarea,
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
......
...@@ -116,8 +116,6 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]: ...@@ -116,8 +116,6 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
url(r'^submit_feedback$', 'util.views.submit_feedback'), url(r'^submit_feedback$', 'util.views.submit_feedback'),
# TODO: These urls no longer work. They need to be updated before they are re-enabled
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
) )
# Only enable URLs for those marketing links actually enabled in the # Only enable URLs for those marketing links actually enabled in the
...@@ -415,6 +413,12 @@ if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): ...@@ -415,6 +413,12 @@ if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
url(r'^instructor_task_status/$', 'instructor_task.views.instructor_task_status', name='instructor_task_status'), url(r'^instructor_task_status/$', 'instructor_task.views.instructor_task_status', name='instructor_task_status'),
) )
if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'):
urlpatterns += (
url(r'^edinsights_service/', include('edinsights.core.urls')),
)
import edinsights.core.registry
# FoldIt views # FoldIt views
urlpatterns += ( urlpatterns += (
# The path is hardcoded into their app... # The path is hardcoded into their app...
...@@ -434,3 +438,5 @@ if settings.DEBUG: ...@@ -434,3 +438,5 @@ if settings.DEBUG:
#Custom error pages #Custom error pages
handler404 = 'static_template_view.views.render_404' handler404 = 'static_template_view.views.render_404'
handler500 = 'static_template_view.views.render_500' handler500 = 'static_template_view.views.render_500'
...@@ -41,6 +41,10 @@ disable= ...@@ -41,6 +41,10 @@ disable=
# W0142: Used * or ** magic # W0142: Used * or ** magic
I0011,C0301,W0141,W0142, I0011,C0301,W0141,W0142,
# Django makes classes that trigger these
# W0232: Class has no __init__ method
W0232,
# Might use these when the code is in better shape # Might use these when the code is in better shape
# C0302: Too many lines in module # C0302: Too many lines in module
# R0201: Method could be a function # R0201: Method could be a function
......
...@@ -3,7 +3,7 @@ begin ...@@ -3,7 +3,7 @@ begin
require 'rake/clean' require 'rake/clean'
require './rakelib/helpers.rb' require './rakelib/helpers.rb'
rescue LoadError => error rescue LoadError => error
puts "Import faild (#{error})" puts "Import failed (#{error})"
puts "Please run `bundle install` to bootstrap ruby dependencies" puts "Please run `bundle install` to bootstrap ruby dependencies"
exit 1 exit 1
end end
......
...@@ -96,13 +96,27 @@ clone_repos() { ...@@ -96,13 +96,27 @@ clone_repos() {
fi fi
} }
set_base_default() { # if PROJECT_HOME not set
# 2 possibilities: this is from cloned repo, or not
# this script is in "./scripts" if a git clone
this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd)
if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then
# set BASE one-up from this_repo;
echo "${this_repo%/*}"
else
echo "$HOME/edx_all"
fi
}
### START ### START
PROG=${0##*/} PROG=${0##*/}
# Adjust this to wherever you'd like to place the codebase # Adjust this to wherever you'd like to place the codebase
BASE="${PROJECT_HOME:-$HOME}/edx_all" BASE="${PROJECT_HOME:-$(set_base_default)}"
# Use a sensible default (~/.virtualenvs) for your Python virtualenvs # Use a sensible default (~/.virtualenvs) for your Python virtualenvs
# unless you've already got one set up with virtualenvwrapper. # unless you've already got one set up with virtualenvwrapper.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment