Merge pull request #1429 from MITx/feature/cale/pep8-autofix

Feature/cale/pep8 autofix
This commit is contained in:
Ned Batchelder
2013-02-06 08:57:55 -08:00
273 changed files with 2403 additions and 1827 deletions

2
.pep8 Normal file
View File

@@ -0,0 +1,2 @@
[pep8]
ignore=E501

View File

@@ -1 +0,0 @@

View File

@@ -18,6 +18,8 @@ STAFF_ROLE_NAME = 'staff'
# we're just making a Django group for each location/role combo # we're just making a Django group for each location/role combo
# to do this we're just creating a Group name which is a formatted string # to do this we're just creating a Group name which is a formatted string
# of those two variables # of those two variables
def get_course_groupname_for_role(location, role): def get_course_groupname_for_role(location, role):
loc = Location(location) loc = Location(location)
# hack: check for existence of a group name in the legacy LMS format <role>_<course> # hack: check for existence of a group name in the legacy LMS format <role>_<course>
@@ -25,11 +27,12 @@ def get_course_groupname_for_role(location, role):
# more information # more information
groupname = '{0}_{1}'.format(role, loc.course) groupname = '{0}_{1}'.format(role, loc.course)
if len(Group.objects.filter(name = groupname)) == 0: if len(Group.objects.filter(name=groupname)) == 0:
groupname = '{0}_{1}'.format(role,loc.course_id) groupname = '{0}_{1}'.format(role, loc.course_id)
return groupname return groupname
def get_users_in_course_group_by_role(location, role): def get_users_in_course_group_by_role(location, role):
groupname = get_course_groupname_for_role(location, role) groupname = get_course_groupname_for_role(location, role)
(group, created) = Group.objects.get_or_create(name=groupname) (group, created) = Group.objects.get_or_create(name=groupname)
@@ -39,6 +42,8 @@ def get_users_in_course_group_by_role(location, role):
''' '''
Create all permission groups for a new course and subscribe the caller into those roles Create all permission groups for a new course and subscribe the caller into those roles
''' '''
def create_all_course_groups(creator, location): def create_all_course_groups(creator, location):
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME) create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
create_new_course_group(creator, location, STAFF_ROLE_NAME) create_new_course_group(creator, location, STAFF_ROLE_NAME)
@@ -46,7 +51,7 @@ def create_all_course_groups(creator, location):
def create_new_course_group(creator, location, role): def create_new_course_group(creator, location, role):
groupname = get_course_groupname_for_role(location, role) groupname = get_course_groupname_for_role(location, role)
(group, created) =Group.objects.get_or_create(name=groupname) (group, created) = Group.objects.get_or_create(name=groupname)
if created: if created:
group.save() group.save()
@@ -59,6 +64,8 @@ def create_new_course_group(creator, location, role):
This is to be called only by either a command line code path or through a app which has already This is to be called only by either a command line code path or through a app which has already
asserted permissions asserted permissions
''' '''
def _delete_course_group(location): def _delete_course_group(location):
# remove all memberships # remove all memberships
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME)) instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
@@ -75,6 +82,8 @@ def _delete_course_group(location):
This is to be called only by either a command line code path or through an app which has already This is to be called only by either a command line code path or through an app which has already
asserted permissions to do this action asserted permissions to do this action
''' '''
def _copy_course_group(source, dest): def _copy_course_group(source, dest):
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME)) instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME)) new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
@@ -133,8 +142,6 @@ def remove_user_from_course_group(caller, user, location, role):
def is_user_in_course_group_role(user, location, role): def is_user_in_course_group_role(user, location, role):
if user.is_active and user.is_authenticated: if user.is_active and user.is_authenticated:
# all "is_staff" flagged accounts belong to all groups # all "is_staff" flagged accounts belong to all groups
return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0 return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
return False return False

View File

@@ -8,6 +8,8 @@ import logging
## TODO store as array of { date, content } and override course_info_module.definition_from_xml ## TODO store as array of { date, content } and override course_info_module.definition_from_xml
## This should be in a class which inherits from XmlDescriptor ## This should be in a class which inherits from XmlDescriptor
def get_course_updates(location): def get_course_updates(location):
""" """
Retrieve the relevant course_info updates and unpack into the model which the client expects: Retrieve the relevant course_info updates and unpack into the model which the client expects:
@@ -42,12 +44,13 @@ def get_course_updates(location):
content = "\n".join([html.tostring(ele) for ele in update[1:]]) content = "\n".join([html.tostring(ele) for ele in update[1:]])
# make the id on the client be 1..len w/ 1 being the oldest and len being the newest # make the id on the client be 1..len w/ 1 being the oldest and len being the newest
course_upd_collection.append({"id" : location_base + "/" + str(len(course_html_parsed) - idx), course_upd_collection.append({"id": location_base + "/" + str(len(course_html_parsed) - idx),
"date" : update.findtext("h2"), "date": update.findtext("h2"),
"content" : content}) "content": content})
return course_upd_collection return course_upd_collection
def update_course_updates(location, update, passed_id=None): def update_course_updates(location, update, passed_id=None):
""" """
Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if
@@ -85,9 +88,10 @@ def update_course_updates(location, update, passed_id=None):
course_updates.definition['data'] = html.tostring(course_html_parsed) course_updates.definition['data'] = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.definition['data']) modulestore('direct').update_item(location, course_updates.definition['data'])
return {"id" : passed_id, return {"id": passed_id,
"date" : update['date'], "date": update['date'],
"content" :update['content']} "content": update['content']}
def delete_course_update(location, update, passed_id): def delete_course_update(location, update, passed_id):
""" """
@@ -124,6 +128,7 @@ def delete_course_update(location, update, passed_id):
return get_course_updates(location) return get_course_updates(location)
def get_idx(passed_id): def get_idx(passed_id):
""" """
From the url w/ idx appended, get the idx. From the url w/ idx appended, get the idx.

View File

@@ -12,6 +12,8 @@ from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
########### 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
@@ -20,25 +22,30 @@ def i_visit_the_studio_homepage(step):
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
assert world.browser.is_element_present_by_css('body.no-header', 10) assert world.browser.is_element_present_by_css('body.no-header', 10)
@step('I am logged into Studio$') @step('I am logged into Studio$')
def i_am_logged_into_studio(step): def i_am_logged_into_studio(step):
log_into_studio() log_into_studio()
@step('I confirm the alert$') @step('I confirm the alert$')
def i_confirm_with_ok(step): def i_confirm_with_ok(step):
world.browser.get_alert().accept() world.browser.get_alert().accept()
@step(u'I press the "([^"]*)" delete icon$') @step(u'I press the "([^"]*)" delete icon$')
def i_press_the_category_delete_icon(step, category): def i_press_the_category_delete_icon(step, category):
if category == 'section': if category == 'section':
css = 'a.delete-button.delete-section-button span.delete-icon' css = 'a.delete-button.delete-section-button span.delete-icon'
elif category == 'subsection': elif category == 'subsection':
css='a.delete-button.delete-subsection-button span.delete-icon' css = 'a.delete-button.delete-subsection-button span.delete-icon'
else: else:
assert False, 'Invalid category: %s' % category assert False, 'Invalid category: %s' % category
css_click(css) css_click(css)
####### HELPER FUNCTIONS ############## ####### HELPER FUNCTIONS ##############
def create_studio_user( def create_studio_user(
uname='robot', uname='robot',
email='robot+studio@edx.org', email='robot+studio@edx.org',
@@ -58,6 +65,7 @@ def create_studio_user(
user_profile = UserProfileFactory(user=studio_user) user_profile = UserProfileFactory(user=studio_user)
def flush_xmodule_store(): def flush_xmodule_store():
# Flush and initialize the module store # Flush and initialize the module store
# It needs the templates because it creates new records # It needs the templates because it creates new records
@@ -70,26 +78,32 @@ def flush_xmodule_store():
xmodule.modulestore.django.modulestore().collection.drop() xmodule.modulestore.django.modulestore().collection.drop()
xmodule.templates.update_templates() xmodule.templates.update_templates()
def assert_css_with_text(css,text):
def assert_css_with_text(css, text):
assert_true(world.browser.is_element_present_by_css(css, 5)) assert_true(world.browser.is_element_present_by_css(css, 5))
assert_equal(world.browser.find_by_css(css).text, text) assert_equal(world.browser.find_by_css(css).text, text)
def css_click(css): def css_click(css):
world.browser.find_by_css(css).first.click() world.browser.find_by_css(css).first.click()
def css_fill(css, value): def css_fill(css, value):
world.browser.find_by_css(css).first.fill(value) world.browser.find_by_css(css).first.fill(value)
def clear_courses(): def clear_courses():
flush_xmodule_store() flush_xmodule_store()
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
org='MITx', org='MITx',
num='101'): num='101'):
css_fill('.new-course-name',name) css_fill('.new-course-name', name)
css_fill('.new-course-org',org) css_fill('.new-course-org', org)
css_fill('.new-course-number',num) css_fill('.new-course-number', num)
def log_into_studio( def log_into_studio(
uname='robot', uname='robot',
@@ -108,20 +122,23 @@ def log_into_studio(
assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) assert_true(world.browser.is_element_present_by_css('.new-course-button', 5))
def create_a_course(): def create_a_course():
css_click('a.new-course-button') css_click('a.new-course-button')
fill_in_course_info() fill_in_course_info()
css_click('input.new-course-save') css_click('input.new-course-save')
assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5)) assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5))
def add_section(name='My Section'): def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
css_click(link_css) css_click(link_css)
name_css = '.new-section-name' name_css = '.new-section-name'
save_css = '.new-section-name-save' save_css = '.new-section-name-save'
css_fill(name_css,name) css_fill(name_css, name)
css_click(save_css) css_click(save_css)
def add_subsection(name='Subsection One'): def add_subsection(name='Subsection One'):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
css_click(css) css_click(css)

View File

@@ -2,49 +2,61 @@ from lettuce import world, step
from common import * from common import *
############### ACTIONS #################### ############### ACTIONS ####################
@step('There are no courses$') @step('There are no courses$')
def no_courses(step): def no_courses(step):
clear_courses() clear_courses()
@step('I click the New Course button$') @step('I click the New Course button$')
def i_click_new_course(step): def i_click_new_course(step):
css_click('.new-course-button') css_click('.new-course-button')
@step('I fill in the new course information$') @step('I fill in the new course information$')
def i_fill_in_a_new_course_information(step): def i_fill_in_a_new_course_information(step):
fill_in_course_info() fill_in_course_info()
@step('I create a new course$') @step('I create a new course$')
def i_create_a_course(step): def i_create_a_course(step):
create_a_course() create_a_course()
@step('I click the course link in My Courses$') @step('I click the course link in My Courses$')
def i_click_the_course_link_in_my_courses(step): def i_click_the_course_link_in_my_courses(step):
course_css = 'span.class-name' course_css = 'span.class-name'
css_click(course_css) css_click(course_css)
############ ASSERTIONS ################### ############ ASSERTIONS ###################
@step('the Courseware page has loaded in Studio$') @step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step): def courseware_page_has_loaded_in_studio(step):
courseware_css = 'a#courseware-tab' courseware_css = 'a#courseware-tab'
assert world.browser.is_element_present_by_css(courseware_css) assert world.browser.is_element_present_by_css(courseware_css)
@step('I see the course listed in My Courses$') @step('I see the course listed in My Courses$')
def i_see_the_course_in_my_courses(step): def i_see_the_course_in_my_courses(step):
course_css = 'span.class-name' course_css = 'span.class-name'
assert_css_with_text(course_css,'Robot Super Course') assert_css_with_text(course_css, 'Robot Super Course')
@step('the course is loaded$') @step('the course is loaded$')
def course_is_loaded(step): def course_is_loaded(step):
class_css = 'a.class-name' class_css = 'a.class-name'
assert_css_with_text(class_css,'Robot Super Course') assert_css_with_text(class_css, 'Robot Super Course')
@step('I am on the "([^"]*)" tab$') @step('I am on the "([^"]*)" tab$')
def i_am_on_tab(step, tab_name): def i_am_on_tab(step, tab_name):
header_css = 'div.inner-wrapper h1' header_css = 'div.inner-wrapper h1'
assert_css_with_text(header_css,tab_name) assert_css_with_text(header_css, tab_name)
@step('I see a link for adding a new section$') @step('I see a link for adding a new section$')
def i_see_new_section_link(step): def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
assert_css_with_text(link_css,'New Section') assert_css_with_text(link_css, 'New Section')

View File

@@ -3,6 +3,7 @@ from student.models import User, UserProfile, Registration
from datetime import datetime from datetime import datetime
import uuid import uuid
class UserProfileFactory(factory.Factory): class UserProfileFactory(factory.Factory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
@@ -10,12 +11,14 @@ class UserProfileFactory(factory.Factory):
name = 'Robot Studio' name = 'Robot Studio'
courseware = 'course.xml' courseware = 'course.xml'
class RegistrationFactory(factory.Factory): class RegistrationFactory(factory.Factory):
FACTORY_FOR = Registration FACTORY_FOR = Registration
user = None user = None
activation_key = uuid.uuid4().hex activation_key = uuid.uuid4().hex
class UserFactory(factory.Factory): class UserFactory(factory.Factory):
FACTORY_FOR = User FACTORY_FOR = User

View File

@@ -2,54 +2,65 @@ from lettuce import world, step
from common import * from common import *
############### ACTIONS #################### ############### ACTIONS ####################
@step('I have opened a new course in Studio$') @step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step): def i_have_opened_a_new_course(step):
clear_courses() clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
@step('I click the new section link$') @step('I click the new section link$')
def i_click_new_section_link(step): def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
css_click(link_css) css_click(link_css)
@step('I enter the section name and click save$') @step('I enter the section name and click save$')
def i_save_section_name(step): def i_save_section_name(step):
name_css = '.new-section-name' name_css = '.new-section-name'
save_css = '.new-section-name-save' save_css = '.new-section-name-save'
css_fill(name_css,'My Section') css_fill(name_css, 'My Section')
css_click(save_css) css_click(save_css)
@step('I have added a new section$') @step('I have added a new section$')
def i_have_added_new_section(step): def i_have_added_new_section(step):
add_section() add_section()
@step('I click the Edit link for the release date$') @step('I click the Edit link for the release date$')
def i_click_the_edit_link_for_the_release_date(step): def i_click_the_edit_link_for_the_release_date(step):
button_css = 'div.section-published-date a.edit-button' button_css = 'div.section-published-date a.edit-button'
css_click(button_css) css_click(button_css)
@step('I save a new section release date$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input' time_css = 'input.start-time.time.ui-timepicker-input'
css_fill(date_css,'12/25/2013') css_fill(date_css, '12/25/2013')
# click here to make the calendar go away # click here to make the calendar go away
css_click(time_css) css_click(time_css)
css_fill(time_css,'12:00am') css_fill(time_css, '12:00am')
css_click('a.save-button') css_click('a.save-button')
############ ASSERTIONS ################### ############ ASSERTIONS ###################
@step('I see my section on the Courseware page$') @step('I see my section on the Courseware page$')
def i_see_my_section_on_the_courseware_page(step): def i_see_my_section_on_the_courseware_page(step):
section_css = 'span.section-name-span' section_css = 'span.section-name-span'
assert_css_with_text(section_css,'My Section') assert_css_with_text(section_css, 'My Section')
@step('the section does not exist$') @step('the section does not exist$')
def section_does_not_exist(step): def section_does_not_exist(step):
css = 'span.section-name-span' css = 'span.section-name-span'
assert world.browser.is_element_not_present_by_css(css) assert world.browser.is_element_not_present_by_css(css)
@step('I see a release date for my section$') @step('I see a release date for my section$')
def i_see_a_release_date_for_my_section(step): def i_see_a_release_date_for_my_section(step):
import re import re
@@ -63,18 +74,21 @@ def i_see_a_release_date_for_my_section(step):
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]' date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]'
time_regex = '[0-2][0-9]:[0-5][0-9]' time_regex = '[0-2][0-9]:[0-5][0-9]'
match_string = '%s %s at %s' % (msg, date_regex, time_regex) match_string = '%s %s at %s' % (msg, date_regex, time_regex)
assert re.match(match_string,status_text) assert re.match(match_string, status_text)
@step('I see a link to create a new subsection$') @step('I see a link to create a new subsection$')
def i_see_a_link_to_create_a_new_subsection(step): def i_see_a_link_to_create_a_new_subsection(step):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
assert world.browser.is_element_present_by_css(css) assert world.browser.is_element_present_by_css(css)
@step('the section release date picker is not visible$') @step('the section release date picker is not visible$')
def the_section_release_date_picker_not_visible(step): def the_section_release_date_picker_not_visible(step):
css = 'div.edit-subsection-publish-settings' css = 'div.edit-subsection-publish-settings'
assert False, world.browser.find_by_css(css).visible assert False, world.browser.find_by_css(css).visible
@step('the section release date is updated$') @step('the section release date is updated$')
def the_section_release_date_is_updated(step): def the_section_release_date_is_updated(step):
css = 'span.published-status' css = 'span.published-status'

View File

@@ -1,5 +1,6 @@
from lettuce import world, step from lettuce import world, step
@step('I fill in the registration form$') @step('I fill in the registration form$')
def i_fill_in_the_registration_form(step): def i_fill_in_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form') register_form = world.browser.find_by_css('form#register_form')
@@ -9,15 +10,18 @@ def i_fill_in_the_registration_form(step):
register_form.find_by_name('name').fill('Robot Studio') register_form.find_by_name('name').fill('Robot Studio')
register_form.find_by_name('terms_of_service').check() register_form.find_by_name('terms_of_service').check()
@step('I press the "([^"]*)" button on the registration form$') @step('I press the "([^"]*)" button on the registration form$')
def i_press_the_button_on_the_registration_form(step, button): def i_press_the_button_on_the_registration_form(step, button):
register_form = world.browser.find_by_css('form#register_form') register_form = world.browser.find_by_css('form#register_form')
register_form.find_by_value(button).click() register_form.find_by_value(button).click()
@step('I should see be on the studio home page$') @step('I should see be on the studio home page$')
def i_should_see_be_on_the_studio_home_page(step): def i_should_see_be_on_the_studio_home_page(step):
assert world.browser.find_by_css('div.inner-wrapper') assert world.browser.find_by_css('div.inner-wrapper')
@step(u'I should see the message "([^"]*)"$') @step(u'I should see the message "([^"]*)"$')
def i_should_see_the_message(step, msg): def i_should_see_the_message(step, msg):
assert world.browser.is_text_present(msg, 5) assert world.browser.is_text_present(msg, 5)

View File

@@ -6,11 +6,13 @@ from nose.tools import assert_true, assert_false, assert_equal
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@step(u'I have a course with no sections$') @step(u'I have a course with no sections$')
def have_a_course(step): def have_a_course(step):
clear_courses() clear_courses()
course = CourseFactory.create() course = CourseFactory.create()
@step(u'I have a course with 1 section$') @step(u'I have a course with 1 section$')
def have_a_course_with_1_section(step): def have_a_course_with_1_section(step):
clear_courses() clear_courses()
@@ -18,8 +20,9 @@ def have_a_course_with_1_section(step):
section = ItemFactory.create(parent_location=course.location) section = ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create( subsection1 = ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template = 'i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name = 'Subsection One',) display_name='Subsection One',)
@step(u'I have a course with multiple sections$') @step(u'I have a course with multiple sections$')
def have_a_course_with_two_sections(step): def have_a_course_with_two_sections(step):
@@ -28,19 +31,20 @@ def have_a_course_with_two_sections(step):
section = ItemFactory.create(parent_location=course.location) section = ItemFactory.create(parent_location=course.location)
subsection1 = ItemFactory.create( subsection1 = ItemFactory.create(
parent_location=section.location, parent_location=section.location,
template = 'i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name = 'Subsection One',) display_name='Subsection One',)
section2 = ItemFactory.create( section2 = ItemFactory.create(
parent_location=course.location, parent_location=course.location,
display_name='Section Two',) display_name='Section Two',)
subsection2 = ItemFactory.create( subsection2 = ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template = 'i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name = 'Subsection Alpha',) display_name='Subsection Alpha',)
subsection3 = ItemFactory.create( subsection3 = ItemFactory.create(
parent_location=section2.location, parent_location=section2.location,
template = 'i4x://edx/templates/sequential/Empty', template='i4x://edx/templates/sequential/Empty',
display_name = 'Subsection Beta',) display_name='Subsection Beta',)
@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):
@@ -48,15 +52,18 @@ def navigate_to_the_course_overview_page(step):
course_locator = '.class-name' course_locator = '.class-name'
css_click(course_locator) css_click(course_locator)
@step(u'I navigate to the courseware page of a course with multiple sections') @step(u'I navigate to the courseware page of a course with multiple sections')
def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step): def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step):
step.given('I have a course with multiple sections') step.given('I have a course with multiple sections')
step.given('I navigate to the course overview page') step.given('I navigate to the course overview page')
@step(u'I add a section') @step(u'I add a section')
def i_add_a_section(step): def i_add_a_section(step):
add_section(name='My New Section That I Just Added') add_section(name='My New Section That I Just Added')
@step(u'I click the "([^"]*)" link$') @step(u'I click the "([^"]*)" link$')
def i_click_the_text_span(step, text): def i_click_the_text_span(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
@@ -65,16 +72,19 @@ def i_click_the_text_span(step, text):
assert_equal(world.browser.find_by_css(span_locator).value, text) assert_equal(world.browser.find_by_css(span_locator).value, text)
css_click(span_locator) css_click(span_locator)
@step(u'I collapse the first section$') @step(u'I collapse the first section$')
def i_collapse_a_section(step): def i_collapse_a_section(step):
collapse_locator = 'section.courseware-section a.collapse' collapse_locator = 'section.courseware-section a.collapse'
css_click(collapse_locator) css_click(collapse_locator)
@step(u'I expand the first section$') @step(u'I expand the first section$')
def i_expand_a_section(step): def i_expand_a_section(step):
expand_locator = 'section.courseware-section a.expand' expand_locator = 'section.courseware-section a.expand'
css_click(expand_locator) css_click(expand_locator)
@step(u'I see the "([^"]*)" link$') @step(u'I see the "([^"]*)" link$')
def i_see_the_span_with_text(step, text): def i_see_the_span_with_text(step, text):
span_locator = '.toggle-button-sections span' span_locator = '.toggle-button-sections span'
@@ -82,6 +92,7 @@ def i_see_the_span_with_text(step, text):
assert_equal(world.browser.find_by_css(span_locator).value, text) assert_equal(world.browser.find_by_css(span_locator).value, text)
assert_true(world.browser.find_by_css(span_locator).visible) assert_true(world.browser.find_by_css(span_locator).visible)
@step(u'I do not see the "([^"]*)" link$') @step(u'I do not see the "([^"]*)" link$')
def i_do_not_see_the_span_with_text(step, text): def i_do_not_see_the_span_with_text(step, text):
# Note that the span will exist on the page but not be visible # Note that the span will exist on the page but not be visible
@@ -89,6 +100,7 @@ def i_do_not_see_the_span_with_text(step, text):
assert_true(world.browser.is_element_present_by_css(span_locator)) assert_true(world.browser.is_element_present_by_css(span_locator))
assert_false(world.browser.find_by_css(span_locator).visible) assert_false(world.browser.find_by_css(span_locator).visible)
@step(u'all sections are expanded$') @step(u'all sections are expanded$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'
@@ -96,6 +108,7 @@ def all_sections_are_expanded(step):
for s in subsections: for s in subsections:
assert_true(s.visible) assert_true(s.visible)
@step(u'all sections are collapsed$') @step(u'all sections are collapsed$')
def all_sections_are_expanded(step): def all_sections_are_expanded(step):
subsection_locator = 'div.subsection-list' subsection_locator = 'div.subsection-list'

View File

@@ -2,6 +2,8 @@ from lettuce import world, step
from common import * from common import *
############### ACTIONS #################### ############### ACTIONS ####################
@step('I have opened a new course section in Studio$') @step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step): def i_have_opened_a_new_course_section(step):
clear_courses() clear_courses()
@@ -9,29 +11,35 @@ def i_have_opened_a_new_course_section(step):
create_a_course() create_a_course()
add_section() add_section()
@step('I click the New Subsection link') @step('I click the New Subsection link')
def i_click_the_new_subsection_link(step): def i_click_the_new_subsection_link(step):
css = 'a.new-subsection-item' css = 'a.new-subsection-item'
css_click(css) css_click(css)
@step('I enter the subsection name and click save$') @step('I enter the subsection name and click save$')
def i_save_subsection_name(step): def i_save_subsection_name(step):
name_css = 'input.new-subsection-name-input' name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
css_fill(name_css,'Subsection One') css_fill(name_css, 'Subsection One')
css_click(save_css) css_click(save_css)
@step('I have added a new subsection$') @step('I have added a new subsection$')
def i_have_added_a_new_subsection(step): def i_have_added_a_new_subsection(step):
add_subsection() add_subsection()
############ ASSERTIONS ################### ############ ASSERTIONS ###################
@step('I see my subsection on the Courseware page$') @step('I see my subsection on the Courseware page$')
def i_see_my_subsection_on_the_courseware_page(step): def i_see_my_subsection_on_the_courseware_page(step):
css = 'span.subsection-name' css = 'span.subsection-name'
assert world.browser.is_element_present_by_css(css) assert world.browser.is_element_present_by_css(css)
css = 'span.subsection-name-value' css = 'span.subsection-name-value'
assert_css_with_text(css,'Subsection One') assert_css_with_text(css, 'Subsection One')
@step('the subsection does not exist$') @step('the subsection does not exist$')
def the_subsection_does_not_exist(step): def the_subsection_does_not_exist(step):

View File

@@ -14,6 +14,7 @@ from auth.authz import _copy_course_group
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3 # To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
# #
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
'''Clone a MongoDB backed course to another location''' '''Clone a MongoDB backed course to another location'''

View File

@@ -15,6 +15,7 @@ from auth.authz import _delete_course_group
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1 # To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
# #
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
'''Delete a MongoDB backed course''' '''Delete a MongoDB backed course'''
@@ -35,6 +36,3 @@ class Command(BaseCommand):
print 'removing User permissions from course....' print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course # in the django layer, we need to remove all the user permissions groups associated with this course
_delete_course_group(loc) _delete_course_group(loc)

View File

@@ -1,5 +1,6 @@
import sys import sys
def query_yes_no(question, default="yes"): def query_yes_no(question, default="yes"):
"""Ask a yes/no question via raw_input() and return their answer. """Ask a yes/no question via raw_input() and return their answer.

View File

@@ -7,7 +7,8 @@ from lxml import etree
import re import re
from django.http import HttpResponseBadRequest, Http404 from django.http import HttpResponseBadRequest, Http404
def get_module_info(store, location, parent_location = None, rewrite_static_links = False):
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
try: try:
if location.revision is None: if location.revision is None:
module = store.get_item(location) module = store.get_item(location)
@@ -36,6 +37,7 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link
'metadata': module.metadata 'metadata': module.metadata
} }
def set_module_info(store, location, post_data): def set_module_info(store, location, post_data):
module = None module = None
isNew = False isNew = False

View File

@@ -5,6 +5,7 @@ from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed) CourseEnrollmentAllowed)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
class UserProfileFactory(Factory): class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
@@ -12,12 +13,14 @@ class UserProfileFactory(Factory):
name = 'Robot Studio' name = 'Robot Studio'
courseware = 'course.xml' courseware = 'course.xml'
class RegistrationFactory(Factory): class RegistrationFactory(Factory):
FACTORY_FOR = Registration FACTORY_FOR = Registration
user = None user = None
activation_key = uuid4().hex activation_key = uuid4().hex
class UserFactory(Factory): class UserFactory(Factory):
FACTORY_FOR = User FACTORY_FOR = User
@@ -32,11 +35,13 @@ class UserFactory(Factory):
last_login = datetime.now() last_login = datetime.now()
date_joined = datetime.now() date_joined = datetime.now()
class GroupFactory(Factory): class GroupFactory(Factory):
FACTORY_FOR = Group FACTORY_FOR = Group
name = 'test_group' name = 'test_group'
class CourseEnrollmentAllowedFactory(Factory): class CourseEnrollmentAllowedFactory(Factory):
FACTORY_FOR = CourseEnrollmentAllowed FACTORY_FOR = CourseEnrollmentAllowed

View File

@@ -35,6 +35,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase): class ContentStoreToyCourseTest(ModuleStoreTestCase):
""" """
@@ -79,7 +80,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') ms = modulestore('direct')
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None])) course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering # reverse the ordering
reverse_tabs = [] reverse_tabs = []
@@ -87,9 +88,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
if tab['type'] == 'static_tab': if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug'])) reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json") resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None])) course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call # compare to make sure that the tabs information is in the expected order after the server call
course_tabs = [] course_tabs = []
@@ -106,12 +107,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
''' '''
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') ms = modulestore('direct')
effort = ms.get_item(Location(['i4x','edX','full','about','effort', None])) effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.definition['data'],'6 hours') self.assertEqual(effort.definition['data'], '6 hours')
# this one should be in a non-override folder # this one should be in a non-override folder
effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None])) effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.definition['data'],'TBD') self.assertEqual(effort.definition['data'], 'TBD')
def test_remove_hide_progress_tab(self): def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -149,12 +150,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# now loop through all the units in the course and verify that the clone can render them, which # now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present # means the objects are at least present
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None])) items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None])) clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0) self.assertGreater(len(clone_items), 0)
for descriptor in items: 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()) 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()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
@@ -169,7 +170,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
delete_course(ms, cs, location) delete_course(ms, cs, location)
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None])) items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0) self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''): def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
@@ -213,7 +214,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# reimport # reimport
import_from_xml(ms, root_dir, ['test_export']) import_from_xml(ms, root_dir, ['test_export'])
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None])) items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
for descriptor in items: for descriptor in items:
print "Checking {0}....".format(descriptor.location.url()) print "Checking {0}....".format(descriptor.location.url())
@@ -229,7 +230,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# import a test course # import a test course
import_from_xml(ms, 'common/test/data/', ['full']) import_from_xml(ms, 'common/test/data/', ['full'])
handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts']) handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
# get module info # get module info
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location})) resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
@@ -365,8 +366,8 @@ class ContentStoreTest(ModuleStoreTestCase):
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
section_data = { section_data = {
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', 'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template' : 'i4x://edx/templates/chapter/Empty', 'template': 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One', 'display_name': 'Section One',
} }
@@ -382,8 +383,8 @@ class ContentStoreTest(ModuleStoreTestCase):
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = { problem_data = {
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', 'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template' : 'i4x://edx/templates/problem/Empty' 'template': 'i4x://edx/templates/problem/Empty'
} }
resp = self.client.post(reverse('clone_item'), problem_data) resp = self.client.post(reverse('clone_item'), problem_data)

View File

@@ -3,6 +3,7 @@ from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from django.test import TestCase from django.test import TestCase
class Content: class Content:
def __init__(self, location, content): def __init__(self, location, content):
self.location = location self.location = location
@@ -11,6 +12,7 @@ class Content:
def get_id(self): def get_id(self):
return StaticContent.get_id_from_location(self.location) return StaticContent.get_id_from_location(self.location)
class CachingTestCase(TestCase): class CachingTestCase(TestCase):
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy # Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg') unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg')

View File

@@ -28,7 +28,7 @@ class ConvertersTestCase(TestCase):
@staticmethod @staticmethod
def struct_to_datetime(struct_time): def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour, return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo = UTC()) struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_dates(self, date1, date2, expected_delta): def compare_dates(self, date1, date2, expected_delta):
dt1 = ConvertersTestCase.struct_to_datetime(date1) dt1 = ConvertersTestCase.struct_to_datetime(date1)
@@ -68,13 +68,14 @@ class CourseTestCase(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
t='i4x://edx/templates/course/Empty' t = 'i4x://edx/templates/course/Empty'
o='MITx' o = 'MITx'
n='999' n = '999'
dn='Robot Super Course' dn = 'Robot Super Course'
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course') self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
CourseFactory.create(template=t, org=o, number=n, display_name=dn) CourseFactory.create(template=t, org=o, number=n, display_name=dn)
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self): def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
@@ -118,6 +119,7 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
jsondetails.effort, "After set effort") jsondetails.effort, "After set effort")
class CourseDetailsViewTest(CourseTestCase): class CourseDetailsViewTest(CourseTestCase):
def alter_field(self, url, details, field, val): def alter_field(self, url, details, field, val):
setattr(details, field, val) setattr(details, field, val)
@@ -141,23 +143,23 @@ class CourseDetailsViewTest(CourseTestCase):
def test_update_and_fetch(self): def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
resp = self.client.get(reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, resp = self.client.get(reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name' : self.course_location.name })) 'name': self.course_location.name}))
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True) self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
# resp s/b json from here on # resp s/b json from here on
url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name' : self.course_location.name, 'section' : 'details' }) 'name': self.course_location.name, 'section': 'details'})
resp = self.client.get(url) resp = self.client.get(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
utc = UTC() utc = UTC()
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc)) self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc)) self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc)) self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc)) self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc)) self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'overview', "Overview") self.alter_field(url, details, 'overview', "Overview")
self.alter_field(url, details, 'intro_video', "intro_video") self.alter_field(url, details, 'intro_video', "intro_video")
self.alter_field(url, details, 'effort', "effort") self.alter_field(url, details, 'effort', "effort")
@@ -190,6 +192,7 @@ class CourseDetailsViewTest(CourseTestCase):
elif field in encoded and encoded[field] is not None: elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context) self.fail(field + " included in encoding but missing from details at " + context)
class CourseGradingTest(CourseTestCase): class CourseGradingTest(CourseTestCase):
def test_initial_grader(self): def test_initial_grader(self):
descriptor = get_modulestore(self.course_location).get_item(self.course_location) descriptor = get_modulestore(self.course_location).get_item(self.course_location)

View File

@@ -2,27 +2,28 @@ from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCas
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import json import json
class CourseUpdateTest(CourseTestCase): class CourseUpdateTest(CourseTestCase):
def test_course_update(self): def test_course_update(self):
# first get the update to force the creation # first get the update to force the creation
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name' : self.course_location.name }) 'name': self.course_location.name})
self.client.get(url) self.client.get(url)
content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>' content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0"></iframe>'
payload = { 'content' : content, payload = {'content': content,
'date' : 'January 8, 2013'} 'date': 'January 8, 2013'}
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'provided_id' : ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload= json.loads(resp.content) payload = json.loads(resp.content)
self.assertHTMLEqual(content, payload['content'], "single iframe") self.assertHTMLEqual(content, payload['content'], "single iframe")
url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'provided_id' : payload['id']}) 'provided_id': payload['id']})
content += '<div>div <p>p</p></div>' content += '<div>div <p>p</p></div>'
payload['content'] = content payload['content'] = content
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")

View File

@@ -2,15 +2,16 @@ from cms.djangoapps.contentstore import utils
import mock import mock
from django.test import TestCase from django.test import TestCase
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
def about_page_test(self): def about_page_test(self):
location = 'i4x','mitX','101','course', 'test' location = 'i4x', 'mitX', '101', 'course', 'test'
utils.get_course_id = mock.Mock(return_value="mitX/101/test") utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_about_page(location) link = utils.get_lms_link_for_about_page(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about") self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
def ls_link_test(self): def ls_link_test(self):
location = 'i4x','mitX','101','vertical', 'contacting_us' location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
utils.get_course_id = mock.Mock(return_value="mitX/101/test") utils.get_course_id = mock.Mock(return_value="mitX/101/test")
link = utils.get_lms_link_for_item(location, False) link = utils.get_lms_link_for_item(location, False)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us") self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")

View File

@@ -28,6 +28,7 @@ from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from utils import ModuleStoreTestCase, parse_json, user, registration from utils import ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase): class ContentStoreTestCase(ModuleStoreTestCase):
def _login(self, email, pw): def _login(self, email, pw):
"""Login. View should always return 200. The success/fail is in the """Login. View should always return 200. The success/fail is in the

View File

@@ -11,6 +11,7 @@ from django.contrib.auth.models import User
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.templates import update_templates from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase): class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb """ Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore module store. This populates a uniquely named modulestore
@@ -22,7 +23,7 @@ class ModuleStoreTestCase(TestCase):
# Use the current seconds since epoch to differentiate # Use the current seconds since epoch to differentiate
# the mongo collections on jenkins. # the mongo collections on jenkins.
sec_since_epoch = '%s' % int(time()*100) sec_since_epoch = '%s' % int(time() * 100)
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE self.test_MODULESTORE = self.orig_MODULESTORE
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
@@ -50,14 +51,17 @@ class ModuleStoreTestCase(TestCase):
super(ModuleStoreTestCase, self)._post_teardown() super(ModuleStoreTestCase, self)._post_teardown()
def parse_json(response): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
return json.loads(response.content) return json.loads(response.content)
def user(email): def user(email):
"""look up a user by email""" """look up a user by email"""
return User.objects.get(email=email) return User.objects.get(email=email)
def registration(email): def registration(email):
"""look up registration object by email""" """look up registration object by email"""
return Registration.objects.get(user__email=email) return Registration.objects.get(user__email=email)

View File

@@ -5,6 +5,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
def get_modulestore(location): def get_modulestore(location):
""" """
Returns the correct modulestore to use for modifying the specified location Returns the correct modulestore to use for modifying the specified location
@@ -17,6 +18,7 @@ def get_modulestore(location):
else: else:
return modulestore() return modulestore()
def get_course_location_for_item(location): def get_course_location_for_item(location):
''' '''
cdodge: for a given Xmodule, return the course that it belongs to cdodge: for a given Xmodule, return the course that it belongs to
@@ -46,6 +48,7 @@ def get_course_location_for_item(location):
return location return location
def get_course_for_item(location): def get_course_for_item(location):
''' '''
cdodge: for a given Xmodule, return the course that it belongs to cdodge: for a given Xmodule, return the course that it belongs to
@@ -85,6 +88,7 @@ def get_lms_link_for_item(location, preview=False):
return lms_link return lms_link
def get_lms_link_for_about_page(location): def get_lms_link_for_about_page(location):
""" """
Returns the url to the course about page from the location tuple. Returns the url to the course about page from the location tuple.
@@ -99,6 +103,7 @@ def get_lms_link_for_about_page(location):
return lms_link return lms_link
def get_course_id(location): def get_course_id(location):
""" """
Returns the course_id from a given the location tuple. Returns the course_id from a given the location tuple.
@@ -106,6 +111,7 @@ def get_course_id(location):
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course # TODO: These will need to be changed to point to the particular instance of this problem in the particular course
return modulestore().get_containing_courses(Location(location))[0].id return modulestore().get_containing_courses(Location(location))[0].id
class UnitState(object): class UnitState(object):
draft = 'draft' draft = 'draft'
private = 'private' private = 'private'
@@ -135,6 +141,7 @@ def compute_unit_state(unit):
def get_date_display(date): def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p") return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value): def update_item(location, value):
""" """
If value is None, delete the db entry. Otherwise, update it using the correct modulestore. If value is None, delete the db entry. Otherwise, update it using the correct modulestore.

View File

@@ -81,6 +81,7 @@ def signup(request):
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token}) return render_to_response('signup.html', {'csrf': csrf_token})
@ssl_login_shortcut @ssl_login_shortcut
@ensure_csrf_cookie @ensure_csrf_cookie
def login_page(request): def login_page(request):
@@ -114,7 +115,7 @@ def index(request):
courses = filter(course_filter, courses) courses = filter(course_filter, courses)
return render_to_response('index.html', { return render_to_response('index.html', {
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.metadata.get('display_name'), 'courses': [(course.metadata.get('display_name'),
reverse('course_index', args=[ reverse('course_index', args=[
course.location.org, course.location.org,
@@ -159,10 +160,10 @@ def course_index(request, org, course, name):
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
upload_asset_callback_url = reverse('upload_asset', kwargs = { upload_asset_callback_url = reverse('upload_asset', kwargs={
'org' : org, 'org': org,
'course' : course, 'course': course,
'coursename' : name 'coursename': name
}) })
course = modulestore().get_item(location) course = modulestore().get_item(location)
@@ -213,7 +214,7 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI # remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems() policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
can_view_live = False can_view_live = False
@@ -233,9 +234,9 @@ def edit_subsection(request, location):
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location, 'parent_location': course.location,
'parent_item': parent, 'parent_item': parent,
'policy_metadata' : policy_metadata, 'policy_metadata': policy_metadata,
'subsection_units' : subsection_units, 'subsection_units': subsection_units,
'can_view_live' : can_view_live 'can_view_live': can_view_live
}) })
@@ -294,7 +295,7 @@ def edit_unit(request, location):
# so let's generate the link url here # so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this # need to figure out where this item is in the list of children as the preview will need this
index =1 index = 1
for child in containing_subsection.get_children(): for child in containing_subsection.get_children():
if child.location == item.location: if child.location == item.location:
break break
@@ -348,6 +349,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@@ -362,7 +364,7 @@ def assignment_type_update(request, org, course, category, name):
if request.method == 'GET': if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)), return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json") mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)), return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json") mimetype="application/json")
@@ -527,7 +529,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
module.get_html, module.get_html,
module.metadata.get('data_dir', module.location.course), module.metadata.get('data_dir', module.location.course),
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]) course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
) )
save_preview_state(request, preview_id, descriptor.location.url(), save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state()) module.get_instance_state(), module.get_shared_state())
@@ -588,7 +590,7 @@ def delete_item(request):
# semantics of delete_item whereby the store is draft aware. Right now calling # semantics of delete_item whereby the store is draft aware. Right now calling
# delete_item on a vertical tries to delete the draft version leaving the # delete_item on a vertical tries to delete the draft version leaving the
# requested delete to never occur # requested delete to never occur
if item.location.revision is None and item.location.category=='vertical' and delete_all_versions: if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location) modulestore('direct').delete_item(item.location)
return HttpResponse() return HttpResponse()
@@ -664,6 +666,7 @@ def create_draft(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def publish_draft(request): def publish_draft(request):
@@ -693,6 +696,7 @@ def unpublish_unit(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def clone_item(request): def clone_item(request):
@@ -725,6 +729,8 @@ def clone_item(request):
#@login_required #@login_required
#@ensure_csrf_cookie #@ensure_csrf_cookie
def upload_asset(request, org, course, coursename): def upload_asset(request, org, course, coursename):
''' '''
cdodge: this method allows for POST uploading of files into the course asset library, which will cdodge: this method allows for POST uploading of files into the course asset library, which will
@@ -775,11 +781,11 @@ def upload_asset(request, org, course, coursename):
# readback the saved content - we need the database timestamp # readback the saved content - we need the database timestamp
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = {'displayname' : content.name, response_payload = {'displayname': content.name,
'uploadDate' : get_date_display(readback.last_modified_at), 'uploadDate': get_date_display(readback.last_modified_at),
'url' : StaticContent.get_url_path_from_location(content.location), 'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg' : 'Upload completed' 'msg': 'Upload completed'
} }
response = HttpResponse(json.dumps(response_payload)) response = HttpResponse(json.dumps(response_payload))
@@ -789,6 +795,8 @@ def upload_asset(request, org, course, coursename):
''' '''
This view will return all CMS users who are editors for the specified course This view will return all CMS users who are editors for the specified course
''' '''
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def manage_users(request, location): def manage_users(request, location):
@@ -803,16 +811,16 @@ def manage_users(request, location):
'active_tab': 'users', 'active_tab': 'users',
'context_course': course_module, 'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME), 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'), 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/'), 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id' : request.user.id 'request_user_id': request.user.id
}) })
def create_json_response(errmsg = None): def create_json_response(errmsg=None):
if errmsg is not None: if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg})) resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else: else:
resp = HttpResponse(json.dumps({'Status': 'OK'})) resp = HttpResponse(json.dumps({'Status': 'OK'}))
@@ -822,13 +830,15 @@ def create_json_response(errmsg = None):
This POST-back view will add a user - specified by email - to the list of editors for This POST-back view will add a user - specified by email - to the list of editors for
the specified course the specified course
''' '''
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def add_user(request, location): def add_user(request, location):
email = request.POST["email"] email = request.POST["email"]
if email=='': if email == '':
return create_json_response('Please specify an email address.') return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course # check that logged in user has admin permissions to this course
@@ -854,6 +864,8 @@ def add_user(request, location):
This POST-back view will remove a user - specified by email - from the list of editors for This POST-back view will remove a user - specified by email - from the list of editors for
the specified course the specified course
''' '''
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@@ -881,6 +893,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename): def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {}) return render_to_response('temp-course-landing.html', {})
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def static_pages(request, org, course, coursename): def static_pages(request, org, course, coursename):
@@ -921,7 +934,7 @@ def reorder_static_tabs(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them # load all reference tabs, return BadRequest if we can't find any of them
tab_items =[] tab_items = []
for tab in tabs: for tab in tabs:
item = modulestore('direct').get_item(Location(tab)) item = modulestore('direct').get_item(Location(tab))
if item is None: if item is None:
@@ -935,8 +948,8 @@ def reorder_static_tabs(request):
for tab in course.tabs: for tab in course.tabs:
if tab['type'] == 'static_tab': if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab', reordered_tabs.append({'type': 'static_tab',
'name' : tab_items[static_tab_idx].metadata.get('display_name'), 'name': tab_items[static_tab_idx].metadata.get('display_name'),
'url_slug' : tab_items[static_tab_idx].location.name}) 'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1 static_tab_idx += 1
else: else:
reordered_tabs.append(tab) reordered_tabs.append(tab)
@@ -980,10 +993,11 @@ def edit_tabs(request, org, course, coursename):
return render_to_response('edit-tabs.html', { return render_to_response('edit-tabs.html', {
'active_tab': 'pages', 'active_tab': 'pages',
'context_course':course_item, 'context_course': course_item,
'components': components 'components': components
}) })
def not_found(request): def not_found(request):
return render_to_response('error.html', {'error': '404'}) return render_to_response('error.html', {'error': '404'})
@@ -1014,11 +1028,12 @@ def course_info(request, org, course, name, provided_id=None):
return render_to_response('course_info.html', { return render_to_response('course_info.html', {
'active_tab': 'courseinfo-tab', 'active_tab': 'courseinfo-tab',
'context_course': course_module, 'context_course': course_module,
'url_base' : "/" + org + "/" + course + "/", 'url_base': "/" + org + "/" + course + "/",
'course_updates' : json.dumps(get_course_updates(location)), 'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@@ -1075,8 +1090,8 @@ def module_info(request, module_location):
else: else:
real_method = request.method real_method = request.method
rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true'] rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links)) logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
@@ -1089,6 +1104,7 @@ def module_info(request, module_location):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def get_course_settings(request, org, course, name): def get_course_settings(request, org, course, name):
@@ -1109,9 +1125,10 @@ def get_course_settings(request, org, course, name):
return render_to_response('settings.html', { return render_to_response('settings.html', {
'active_tab': 'settings', 'active_tab': 'settings',
'context_course': course_module, 'context_course': course_module,
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder) 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@@ -1137,12 +1154,13 @@ def course_settings_updates(request, org, course, name, section):
if request.method == 'GET': if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@@ -1167,14 +1185,14 @@ def course_grader_updates(request, org, course, name, grader_index=None):
if real_method == 'GET': if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)), return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
mimetype="application/json") mimetype="application/json")
elif real_method == "DELETE": elif real_method == "DELETE":
# ??? Shoudl this return anything? Perhaps success fail? # ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index) CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
return HttpResponse() return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course',name]), request.POST)), return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
mimetype="application/json") mimetype="application/json")
@@ -1193,10 +1211,10 @@ def asset_index(request, org, course, name):
raise PermissionDenied() raise PermissionDenied()
upload_asset_callback_url = reverse('upload_asset', kwargs = { upload_asset_callback_url = reverse('upload_asset', kwargs={
'org' : org, 'org': org,
'course' : course, 'course': course,
'coursename' : name 'coursename': name
}) })
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
@@ -1237,6 +1255,7 @@ def asset_index(request, org, course, name):
def edge(request): def edge(request):
return render_to_response('university_profiles/edge.html', {}) return render_to_response('university_profiles/edge.html', {})
@login_required @login_required
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
@@ -1288,6 +1307,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()})) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course): def initialize_course_tabs(course):
# set up the default tabs # set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
@@ -1305,6 +1325,7 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), course.own_metadata) modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
@@ -1343,7 +1364,7 @@ def import_course(request, org, course, name):
# find the 'course.xml' file # find the 'course.xml' file
for r,d,f in os.walk(course_dir): for r, d, f in os.walk(course_dir):
for files in f: for files in f:
if files == 'course.xml': if files == 'course.xml':
break break
@@ -1357,10 +1378,10 @@ def import_course(request, org, course, name):
if r != course_dir: if r != course_dir:
for fname in os.listdir(r): for fname in os.listdir(r):
shutil.move(r/fname, course_dir) shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location)) [course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
# we can blow this away when we're done importing. # we can blow this away when we're done importing.
shutil.rmtree(course_dir) shutil.rmtree(course_dir)
@@ -1376,12 +1397,13 @@ def import_course(request, org, course, name):
return render_to_response('import.html', { return render_to_response('import.html', {
'context_course': course_module, 'context_course': course_module,
'active_tab': 'import', 'active_tab': 'import',
'successful_import_redirect_url' : reverse('course_index', args=[ 'successful_import_redirect_url': reverse('course_index', args=[
course_module.location.org, course_module.location.org,
course_module.location.course, course_module.location.course,
course_module.location.name]) course_module.location.name])
}) })
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def generate_export_course(request, org, course, name): def generate_export_course(request, org, course, name):
@@ -1391,7 +1413,7 @@ def generate_export_course(request, org, course, name):
raise PermissionDenied() raise PermissionDenied()
loc = Location(location) loc = Location(location)
export_file = NamedTemporaryFile(prefix=name+'.', suffix=".tar.gz") export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp()) root_dir = path(mkdtemp())
@@ -1404,11 +1426,11 @@ def generate_export_course(request, org, course, name):
logging.debug('tar file being generated at {0}'.format(export_file.name)) logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz') tf = tarfile.open(name=export_file.name, mode='w:gz')
tf.add(root_dir/name, arcname=name) tf.add(root_dir / name, arcname=name)
tf.close() tf.close()
# remove temp dir # remove temp dir
shutil.rmtree(root_dir/name) shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file) wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz') response = HttpResponse(wrapper, content_type='application/x-tgz')
@@ -1430,9 +1452,10 @@ def export_course(request, org, course, name):
return render_to_response('export.html', { return render_to_response('export.html', {
'context_course': course_module, 'context_course': course_module,
'active_tab': 'export', 'active_tab': 'export',
'successful_import_redirect_url' : '' 'successful_import_redirect_url': ''
}) })
def event(request): def event(request):
''' '''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at

View File

@@ -10,7 +10,7 @@ class CourseGradingModel(object):
""" """
def __init__(self, course_descriptor): def __init__(self, course_descriptor):
self.course_location = course_descriptor.location self.course_location = course_descriptor.location
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
@@ -47,12 +47,12 @@ class CourseGradingModel(object):
# return empty model # return empty model
else: else:
return { return {
"id" : index, "id": index,
"type" : "", "type": "",
"min_count" : 0, "min_count": 0,
"drop_count" : 0, "drop_count": 0,
"short_label" : None, "short_label": None,
"weight" : 0 "weight": 0
} }
@staticmethod @staticmethod
@@ -75,7 +75,7 @@ class CourseGradingModel(object):
course_location = Location(course_location) course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
return {'grace_period' : CourseGradingModel.convert_set_grace_period(descriptor) } return {'grace_period': CourseGradingModel.convert_set_grace_period(descriptor)}
@staticmethod @staticmethod
def update_from_json(jsondict): def update_from_json(jsondict):
@@ -211,9 +211,9 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return { return {
"graderType" : descriptor.metadata.get('format', u"Not Graded"), "graderType": descriptor.metadata.get('format', u"Not Graded"),
"location" : location, "location": location,
"id" : 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
@staticmethod @staticmethod
@@ -245,11 +245,11 @@ class CourseGradingModel(object):
def parse_grader(json_grader): def parse_grader(json_grader):
# manual to clear out kruft # manual to clear out kruft
result = { result = {
"type" : json_grader["type"], "type": json_grader["type"],
"min_count" : int(json_grader.get('min_count', 0)), "min_count": int(json_grader.get('min_count', 0)),
"drop_count" : int(json_grader.get('drop_count', 0)), "drop_count": int(json_grader.get('drop_count', 0)),
"short_label" : json_grader.get('short_label', None), "short_label": json_grader.get('short_label', None),
"weight" : float(json_grader.get('weight', 0)) / 100.0 "weight": float(json_grader.get('weight', 0)) / 100.0
} }
return result return result

View File

@@ -33,8 +33,8 @@ MITX_FEATURES = {
'USE_DJANGO_PIPELINE': True, 'USE_DJANGO_PIPELINE': True,
'GITHUB_PUSH': False, 'GITHUB_PUSH': False,
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES' : False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
@@ -229,7 +229,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
) + [ 'js/hesitate.js', 'js/base.js'], ) + ['js/hesitate.js', 'js/base.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
}, },
'module-js': { 'module-js': {

View File

@@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log", LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev", logging_env="dev",
tracking_filename="tracking.log", tracking_filename="tracking.log",
dev_env = True, dev_env=True,
debug=True) debug=True)
modulestore_options = { modulestore_options = {
@@ -41,7 +41,7 @@ CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': { 'OPTIONS': {
'host': 'localhost', 'host': 'localhost',
'db' : 'xcontent', 'db': 'xcontent',
} }
} }

View File

@@ -9,8 +9,6 @@ import socket
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss MITX_FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy

View File

@@ -19,7 +19,7 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = path('test_root') TEST_ROOT = path('test_root')
# Makes the tests run much faster... # Makes the tests run much faster...
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Want static files in the same dir for running on jenkins. # Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles" STATIC_ROOT = TEST_ROOT / "staticfiles"
@@ -62,7 +62,7 @@ CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': { 'OPTIONS': {
'host': 'localhost', 'host': 'localhost',
'db' : 'xcontent', 'db': 'xcontent',
} }
} }

View File

@@ -2,7 +2,7 @@
from django.core.management import execute_manager from django.core.management import execute_manager
import imp import imp
try: try:
imp.find_module('settings') # Assumed to be in the same directory. imp.find_module('settings') # Assumed to be in the same directory.
except ImportError: except ImportError:
import sys import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. " sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. "

View File

@@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS
from . import app_settings from . import app_settings
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
def get_instance(model, instance_or_pk, timeout=None, using=None): def get_instance(model, instance_or_pk, timeout=None, using=None):
""" """
Returns the ``model`` instance with a primary key of ``instance_or_pk``. Returns the ``model`` instance with a primary key of ``instance_or_pk``.
@@ -108,11 +109,14 @@ def instance_key(model, instance_or_pk):
getattr(instance_or_pk, 'pk', instance_or_pk), getattr(instance_or_pk, 'pk', instance_or_pk),
) )
def set_cached_content(content): def set_cached_content(content):
cache.set(str(content.location), content) cache.set(str(content.location), content)
def get_cached_content(location): def get_cached_content(location):
return cache.get(str(location)) return cache.get(str(location))
def del_cached_content(location): def del_cached_content(location):
cache.delete(str(location)) cache.delete(str(location))

View File

@@ -12,7 +12,7 @@ from xmodule.exceptions import NotFoundError
class StaticContentServer(object): class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag # look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_TAG +'/'): if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'):
loc = StaticContent.get_location_from_path(request.path) loc = StaticContent.get_location_from_path(request.path)
# first look in our cache so we don't have to round-trip to the DB # first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(loc) content = get_cached_content(loc)

View File

@@ -13,6 +13,7 @@ from .models import CourseUserGroup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def is_course_cohorted(course_id): def is_course_cohorted(course_id):
""" """
Given a course id, return a boolean for whether or not the course is Given a course id, return a boolean for whether or not the course is
@@ -115,6 +116,7 @@ def get_course_cohorts(course_id):
### Helpers for cohort management views ### Helpers for cohort management views
def get_cohort_by_name(course_id, name): def get_cohort_by_name(course_id, name):
""" """
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
@@ -124,6 +126,7 @@ def get_cohort_by_name(course_id, name):
group_type=CourseUserGroup.COHORT, group_type=CourseUserGroup.COHORT,
name=name) name=name)
def get_cohort_by_id(course_id, cohort_id): def get_cohort_by_id(course_id, cohort_id):
""" """
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
@@ -133,6 +136,7 @@ def get_cohort_by_id(course_id, cohort_id):
group_type=CourseUserGroup.COHORT, group_type=CourseUserGroup.COHORT,
id=cohort_id) id=cohort_id)
def add_cohort(course_id, name): def add_cohort(course_id, name):
""" """
Add a cohort to a course. Raises ValueError if a cohort of the same name already Add a cohort to a course. Raises ValueError if a cohort of the same name already
@@ -148,12 +152,14 @@ def add_cohort(course_id, name):
group_type=CourseUserGroup.COHORT, group_type=CourseUserGroup.COHORT,
name=name) name=name)
class CohortConflict(Exception): class CohortConflict(Exception):
""" """
Raised when user to be added is already in another cohort in same course. Raised when user to be added is already in another cohort in same course.
""" """
pass pass
def add_user_to_cohort(cohort, username_or_email): def add_user_to_cohort(cohort, username_or_email):
""" """
Look up the given user, and if successful, add them to the specified cohort. Look up the given user, and if successful, add them to the specified cohort.
@@ -211,4 +217,3 @@ def delete_empty_cohort(course_id, name):
name, course_id)) name, course_id))
cohort.delete() cohort.delete()

View File

@@ -5,6 +5,7 @@ from django.db import models
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseUserGroup(models.Model): class CourseUserGroup(models.Model):
""" """
This model represents groups of users in a course. Groups may have different types, This model represents groups of users in a course. Groups may have different types,
@@ -30,5 +31,3 @@ class CourseUserGroup(models.Model):
COHORT = 'cohort' COHORT = 'cohort'
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),) GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES) group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)

View File

@@ -14,6 +14,7 @@ from xmodule.modulestore.django import modulestore, _MODULESTORES
# manually overriding the modulestore. However, running with # manually overriding the modulestore. However, running with
# cms.envs.test doesn't. # cms.envs.test doesn't.
def xml_store_config(data_dir): def xml_store_config(data_dir):
return { return {
'default': { 'default': {
@@ -28,6 +29,7 @@ def xml_store_config(data_dir):
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase): class TestCohorts(django.test.TestCase):
@@ -184,6 +186,3 @@ class TestCohorts(django.test.TestCase):
self.assertTrue( self.assertTrue(
is_commentable_cohorted(course.id, to_id("Feedback")), is_commentable_cohorted(course.id, to_id("Feedback")),
"Feedback was listed as cohorted. Should be.") "Feedback was listed as cohorted. Should be.")

View File

@@ -22,6 +22,7 @@ import track.views
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def json_http_response(data): def json_http_response(data):
""" """
Return an HttpResponse with the data json-serialized and the right content Return an HttpResponse with the data json-serialized and the right content
@@ -29,6 +30,7 @@ def json_http_response(data):
""" """
return HttpResponse(json.dumps(data), content_type="application/json") return HttpResponse(json.dumps(data), content_type="application/json")
def split_by_comma_and_whitespace(s): def split_by_comma_and_whitespace(s):
""" """
Split a string both by commas and whitespice. Returns a list. Split a string both by commas and whitespice. Returns a list.
@@ -177,6 +179,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
'conflict': conflict, 'conflict': conflict,
'unknown': unknown}) 'unknown': unknown})
@ensure_csrf_cookie @ensure_csrf_cookie
@require_POST @require_POST
def remove_user_from_cohort(request, course_id, cohort_id): def remove_user_from_cohort(request, course_id, cohort_id):

View File

@@ -5,8 +5,9 @@ django admin pages for courseware model
from external_auth.models import * from external_auth.models import *
from django.contrib import admin from django.contrib import admin
class ExternalAuthMapAdmin(admin.ModelAdmin): class ExternalAuthMapAdmin(admin.ModelAdmin):
search_fields = ['external_id','user__username'] search_fields = ['external_id', 'user__username']
date_hierarchy = 'dtcreated' date_hierarchy = 'dtcreated'
admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin) admin.site.register(ExternalAuthMap, ExternalAuthMapAdmin)

View File

@@ -12,6 +12,7 @@ file and check it in at the same time as your model changes. To do that,
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
class ExternalAuthMap(models.Model): class ExternalAuthMap(models.Model):
class Meta: class Meta:
unique_together = (('external_id', 'external_domain'), ) unique_together = (('external_id', 'external_domain'), )
@@ -19,13 +20,12 @@ class ExternalAuthMap(models.Model):
external_domain = models.CharField(max_length=255, db_index=True) external_domain = models.CharField(max_length=255, db_index=True)
external_credentials = models.TextField(blank=True) # JSON dictionary external_credentials = models.TextField(blank=True) # JSON dictionary
external_email = models.CharField(max_length=255, db_index=True) external_email = models.CharField(max_length=255, db_index=True)
external_name = models.CharField(blank=True,max_length=255, db_index=True) external_name = models.CharField(blank=True, max_length=255, db_index=True)
user = models.OneToOneField(User, unique=True, db_index=True, null=True) user = models.OneToOneField(User, unique=True, db_index=True, null=True)
internal_password = models.CharField(blank=True, max_length=31) # randomly generated internal_password = models.CharField(blank=True, max_length=31) # randomly generated
dtcreated = models.DateTimeField('creation date',auto_now_add=True) dtcreated = models.DateTimeField('creation date', auto_now_add=True)
dtsignup = models.DateTimeField('signup date',null=True) # set after signup dtsignup = models.DateTimeField('signup date', null=True) # set after signup
def __unicode__(self): def __unicode__(self):
s = "[%s] = (%s / %s)" % (self.external_id, self.external_name, self.external_email) s = "[%s] = (%s / %s)" % (self.external_id, self.external_name, self.external_email)
return s return s

View File

@@ -13,6 +13,7 @@ from django.test import TestCase, LiveServerTestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import RequestFactory from django.test.client import RequestFactory
class MyFetcher(HTTPFetcher): class MyFetcher(HTTPFetcher):
"""A fetcher that uses server-internal calls for performing HTTP """A fetcher that uses server-internal calls for performing HTTP
requests. requests.
@@ -60,6 +61,7 @@ class MyFetcher(HTTPFetcher):
status=status, status=status,
) )
class OpenIdProviderTest(TestCase): class OpenIdProviderTest(TestCase):
# def setUp(self): # def setUp(self):
@@ -78,7 +80,7 @@ class OpenIdProviderTest(TestCase):
provider_url = reverse('openid-provider-xrds') provider_url = reverse('openid-provider-xrds')
factory = RequestFactory() factory = RequestFactory()
request = factory.request() request = factory.request()
abs_provider_url = request.build_absolute_uri(location = provider_url) abs_provider_url = request.build_absolute_uri(location=provider_url)
# In order for this absolute URL to work (i.e. to get xrds, then authentication) # In order for this absolute URL to work (i.e. to get xrds, then authentication)
# in the test environment, we either need a live server that works with the default # in the test environment, we either need a live server that works with the default
@@ -89,7 +91,7 @@ class OpenIdProviderTest(TestCase):
# now we can begin the login process by invoking a local openid client, # now we can begin the login process by invoking a local openid client,
# with a pointer to the (also-local) openid provider: # with a pointer to the (also-local) openid provider:
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url): with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
url = reverse('openid-login') url = reverse('openid-login')
resp = self.client.post(url) resp = self.client.post(url)
code = 200 code = 200
@@ -107,7 +109,7 @@ class OpenIdProviderTest(TestCase):
provider_url = reverse('openid-provider-login') provider_url = reverse('openid-provider-login')
factory = RequestFactory() factory = RequestFactory()
request = factory.request() request = factory.request()
abs_provider_url = request.build_absolute_uri(location = provider_url) abs_provider_url = request.build_absolute_uri(location=provider_url)
# In order for this absolute URL to work (i.e. to get xrds, then authentication) # In order for this absolute URL to work (i.e. to get xrds, then authentication)
# in the test environment, we either need a live server that works with the default # in the test environment, we either need a live server that works with the default
@@ -118,7 +120,7 @@ class OpenIdProviderTest(TestCase):
# now we can begin the login process by invoking a local openid client, # now we can begin the login process by invoking a local openid client,
# with a pointer to the (also-local) openid provider: # with a pointer to the (also-local) openid provider:
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url): with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
url = reverse('openid-login') url = reverse('openid-login')
resp = self.client.post(url) resp = self.client.post(url)
code = 200 code = 200
@@ -154,24 +156,24 @@ class OpenIdProviderTest(TestCase):
return return
url = reverse('openid-provider-login') url = reverse('openid-provider-login')
post_args = { post_args = {
"openid.mode" : "checkid_setup", "openid.mode": "checkid_setup",
"openid.return_to" : "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H", "openid.return_to": "http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H",
"openid.assoc_handle" : "{HMAC-SHA1}{50ff8120}{rh87+Q==}", "openid.assoc_handle": "{HMAC-SHA1}{50ff8120}{rh87+Q==}",
"openid.claimed_id" : "http://specs.openid.net/auth/2.0/identifier_select", "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
"openid.ns" : "http://specs.openid.net/auth/2.0", "openid.ns": "http://specs.openid.net/auth/2.0",
"openid.realm" : "http://testserver/", "openid.realm": "http://testserver/",
"openid.identity" : "http://specs.openid.net/auth/2.0/identifier_select", "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
"openid.ns.ax" : "http://openid.net/srv/ax/1.0", "openid.ns.ax": "http://openid.net/srv/ax/1.0",
"openid.ax.mode" : "fetch_request", "openid.ax.mode": "fetch_request",
"openid.ax.required" : "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname", "openid.ax.required": "email,fullname,old_email,firstname,old_nickname,lastname,old_fullname,nickname",
"openid.ax.type.fullname" : "http://axschema.org/namePerson", "openid.ax.type.fullname": "http://axschema.org/namePerson",
"openid.ax.type.lastname" : "http://axschema.org/namePerson/last", "openid.ax.type.lastname": "http://axschema.org/namePerson/last",
"openid.ax.type.firstname" : "http://axschema.org/namePerson/first", "openid.ax.type.firstname": "http://axschema.org/namePerson/first",
"openid.ax.type.nickname" : "http://axschema.org/namePerson/friendly", "openid.ax.type.nickname": "http://axschema.org/namePerson/friendly",
"openid.ax.type.email" : "http://axschema.org/contact/email", "openid.ax.type.email": "http://axschema.org/contact/email",
"openid.ax.type.old_email" : "http://schema.openid.net/contact/email", "openid.ax.type.old_email": "http://schema.openid.net/contact/email",
"openid.ax.type.old_nickname" : "http://schema.openid.net/namePerson/friendly", "openid.ax.type.old_nickname": "http://schema.openid.net/namePerson/friendly",
"openid.ax.type.old_fullname" : "http://schema.openid.net/namePerson", "openid.ax.type.old_fullname": "http://schema.openid.net/namePerson",
} }
resp = self.client.post(url, post_args) resp = self.client.post(url, post_args)
code = 200 code = 200
@@ -196,11 +198,11 @@ class OpenIdProviderLiveServerTest(LiveServerTestCase):
provider_url = reverse('openid-provider-xrds') provider_url = reverse('openid-provider-xrds')
factory = RequestFactory() factory = RequestFactory()
request = factory.request() request = factory.request()
abs_provider_url = request.build_absolute_uri(location = provider_url) abs_provider_url = request.build_absolute_uri(location=provider_url)
# now we can begin the login process by invoking a local openid client, # now we can begin the login process by invoking a local openid client,
# with a pointer to the (also-local) openid provider: # with a pointer to the (also-local) openid provider:
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url): with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
url = reverse('openid-login') url = reverse('openid-login')
resp = self.client.post(url) resp = self.client.post(url)
code = 200 code = 200

View File

@@ -12,6 +12,7 @@ import mitxmako.middleware
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MakoLoader(object): class MakoLoader(object):
""" """
This is a Django loader object which will load the template as a This is a Django loader object which will load the template as a
@@ -71,6 +72,7 @@ class MakoFilesystemLoader(MakoLoader):
def __init__(self): def __init__(self):
MakoLoader.__init__(self, FilesystemLoader()) MakoLoader.__init__(self, FilesystemLoader())
class MakoAppDirectoriesLoader(MakoLoader): class MakoAppDirectoriesLoader(MakoLoader):
is_usable = True is_usable = True

View File

@@ -20,6 +20,8 @@ from mitxmako import middleware
django_variables = ['lookup', 'output_encoding', 'encoding_errors'] django_variables = ['lookup', 'output_encoding', 'encoding_errors']
# TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate) # TODO: We should make this a Django Template subclass that simply has the MakoTemplate inside of it? (Intead of inheriting from MakoTemplate)
class Template(MakoTemplate): class Template(MakoTemplate):
""" """
This bridges the gap between a Mako template and a djano template. It can This bridges the gap between a Mako template and a djano template. It can

View File

@@ -2,6 +2,7 @@ from django.template import loader
from django.template.base import Template, Context from django.template.base import Template, Context
from django.template.loader import get_template, select_template from django.template.loader import get_template, select_template
def django_template_include(file_name, mako_context): def django_template_include(file_name, mako_context):
""" """
This can be used within a mako template to include a django template This can be used within a mako template to include a django template
@@ -9,7 +10,7 @@ def django_template_include(file_name, mako_context):
which can be the mako context ('context') or a dictionary. which can be the mako context ('context') or a dictionary.
""" """
dictionary = dict( mako_context ) dictionary = dict(mako_context)
return loader.render_to_string(file_name, dictionary=dictionary) return loader.render_to_string(file_name, dictionary=dictionary)
@@ -49,5 +50,3 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
new_context['csrf_token'] = csrf_token new_context['csrf_token'] = csrf_token
return nodelist.render(new_context) return nodelist.render(new_context)

View File

@@ -21,6 +21,7 @@ def _url_replace_regex(prefix):
(?P=quote) # the first matching closing quote (?P=quote) # the first matching closing quote
""".format(prefix=prefix) """.format(prefix=prefix)
def try_staticfiles_lookup(path): def try_staticfiles_lookup(path):
""" """
Try to lookup a path in staticfiles_storage. If it fails, return Try to lookup a path in staticfiles_storage. If it fails, return

View File

@@ -53,6 +53,7 @@ def test_mongo_filestore(mock_modulestore, mock_static_content):
mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE) mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE)
@patch('static_replace.settings') @patch('static_replace.settings')
@patch('static_replace.modulestore') @patch('static_replace.modulestore')
@patch('static_replace.staticfiles_storage') @patch('static_replace.staticfiles_storage')

View File

@@ -1 +0,0 @@

View File

@@ -10,6 +10,7 @@ import sys
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_site_status_msg(course_id): def get_site_status_msg(course_id):
""" """
Look for a file settings.STATUS_MESSAGE_PATH. If found, read it, Look for a file settings.STATUS_MESSAGE_PATH. If found, read it,

View File

@@ -57,7 +57,7 @@ from student.userprofile. '''
d[key] = item d[key] = item
return d return d
extracted = [{'up':extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples] extracted = [{'up': extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples]
fp = open('transfer_users.txt', 'w') fp = open('transfer_users.txt', 'w')
json.dump(extracted, fp) json.dump(extracted, fp)
fp.close() fp.close()

View File

@@ -3,6 +3,7 @@ from optparse import make_option
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
make_option('--list', make_option('--list',

View File

@@ -8,6 +8,7 @@ from student.models import UserProfile, CourseEnrollment
from student.views import _do_create_account, get_random_post_override from student.views import _do_create_account, get_random_post_override
def create(n, course_id): def create(n, course_id):
"""Create n users, enrolling them in course_id if it's not None""" """Create n users, enrolling them in course_id if it's not None"""
for i in range(n): for i in range(n):
@@ -15,6 +16,7 @@ def create(n, course_id):
if course_id is not None: if course_id is not None:
CourseEnrollment.objects.create(user=user, course_id=course_id) CourseEnrollment.objects.create(user=user, course_id=course_id)
class Command(BaseCommand): class Command(BaseCommand):
help = """Create N new users, with random parameters. help = """Create N new users, with random parameters.

View File

@@ -49,19 +49,19 @@ class Command(BaseCommand):
for registration in registrations: for registration in registrations:
if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending: if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending:
continue continue
record = {'username' : registration.testcenter_user.user.username, record = {'username': registration.testcenter_user.user.username,
'email' : registration.testcenter_user.email, 'email': registration.testcenter_user.email,
'first_name' : registration.testcenter_user.first_name, 'first_name': registration.testcenter_user.first_name,
'last_name' : registration.testcenter_user.last_name, 'last_name': registration.testcenter_user.last_name,
'client_candidate_id' : registration.client_candidate_id, 'client_candidate_id': registration.client_candidate_id,
'client_authorization_id' : registration.client_authorization_id, 'client_authorization_id': registration.client_authorization_id,
'course_id' : registration.course_id, 'course_id': registration.course_id,
'exam_series_code' : registration.exam_series_code, 'exam_series_code': registration.exam_series_code,
'accommodation_request' : registration.accommodation_request, 'accommodation_request': registration.accommodation_request,
'accommodation_code' : registration.accommodation_code, 'accommodation_code': registration.accommodation_code,
'registration_status' : registration.registration_status(), 'registration_status': registration.registration_status(),
'demographics_status' : registration.demographics_status(), 'demographics_status': registration.demographics_status(),
'accommodation_status' : registration.accommodation_status(), 'accommodation_status': registration.accommodation_status(),
} }
if len(registration.upload_error_message) > 0: if len(registration.upload_error_message) > 0:
record['registration_error'] = registration.upload_error_message record['registration_error'] = registration.upload_error_message
@@ -75,4 +75,3 @@ class Command(BaseCommand):
# dump output: # dump output:
with open(outputfile, 'w') as outfile: with open(outputfile, 'w') as outfile:
dump(output, outfile, indent=2) dump(output, outfile, indent=2)

View File

@@ -100,7 +100,7 @@ class Command(BaseCommand):
extrasaction='ignore') extrasaction='ignore')
writer.writeheader() writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'): for tcu in TestCenterUser.objects.order_by('id'):
if tcu.needs_uploading: # or dump_all if tcu.needs_uploading: # or dump_all
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field))) record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
for csv_field, model_field for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items()) in Command.CSV_TO_MODEL_FIELDS.items())

View File

@@ -116,4 +116,3 @@ class Command(BaseCommand):
tcuser.save() tcuser.save()
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name) Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name)

View File

@@ -9,6 +9,7 @@ from student.views import course_from_id
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
# registration info: # registration info:
@@ -120,9 +121,9 @@ class Command(BaseCommand):
else: else:
# otherwise use explicit values (so we don't have to define a course): # otherwise use explicit values (so we don't have to define a course):
exam_name = "Dummy Placeholder Name" exam_name = "Dummy Placeholder Name"
exam_info = { 'Exam_Series_Code': our_options['exam_series_code'], exam_info = {'Exam_Series_Code': our_options['exam_series_code'],
'First_Eligible_Appointment_Date' : our_options['eligibility_appointment_date_first'], 'First_Eligible_Appointment_Date': our_options['eligibility_appointment_date_first'],
'Last_Eligible_Appointment_Date' : our_options['eligibility_appointment_date_last'], 'Last_Eligible_Appointment_Date': our_options['eligibility_appointment_date_last'],
} }
exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info) exam = CourseDescriptor.TestCenterExam(course_id, exam_name, exam_info)
# update option values for date_first and date_last to use YYYY-MM-DD format # update option values for date_first and date_last to use YYYY-MM-DD format
@@ -135,7 +136,7 @@ class Command(BaseCommand):
exam_code = exam.exam_series_code exam_code = exam.exam_series_code
UPDATE_FIELDS = ( 'accommodation_request', UPDATE_FIELDS = ('accommodation_request',
'accommodation_code', 'accommodation_code',
'client_authorization_id', 'client_authorization_id',
'exam_series_code', 'exam_series_code',
@@ -152,7 +153,7 @@ class Command(BaseCommand):
if fieldname in our_options and registration.__getattribute__(fieldname) != our_options[fieldname]: if fieldname in our_options and registration.__getattribute__(fieldname) != our_options[fieldname]:
needs_updating = True; needs_updating = True;
else: else:
accommodation_request = our_options.get('accommodation_request','') accommodation_request = our_options.get('accommodation_request', '')
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request) registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
needs_updating = True needs_updating = True
@@ -194,7 +195,7 @@ class Command(BaseCommand):
if 'exam_series_code' in our_options: if 'exam_series_code' in our_options:
exam_code = our_options['exam_series_code'] exam_code = our_options['exam_series_code']
registration = get_testcenter_registration(student, course_id, exam_code)[0] registration = get_testcenter_registration(student, course_id, exam_code)[0]
for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id']: for internal_field in ['upload_error_message', 'upload_status', 'authorization_id']:
if internal_field in our_options: if internal_field in our_options:
registration.__setattr__(internal_field, our_options[internal_field]) registration.__setattr__(internal_field, our_options[internal_field])
change_internal = True change_internal = True
@@ -204,5 +205,3 @@ class Command(BaseCommand):
registration.save() registration.save()
else: else:
print "No changes necessary to make to confirmation information in existing user's registration." print "No changes necessary to make to confirmation information in existing user's registration."

View File

@@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser, TestCenterUserForm from student.models import TestCenterUser, TestCenterUserForm
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
# demographics: # demographics:
@@ -177,7 +178,7 @@ class Command(BaseCommand):
# override internal values: # override internal values:
change_internal = False change_internal = False
testcenter_user = TestCenterUser.objects.get(user=student) testcenter_user = TestCenterUser.objects.get(user=student)
for internal_field in [ 'upload_error_message', 'upload_status', 'client_candidate_id']: for internal_field in ['upload_error_message', 'upload_status', 'client_candidate_id']:
if internal_field in our_options: if internal_field in our_options:
testcenter_user.__setattr__(internal_field, our_options[internal_field]) testcenter_user.__setattr__(internal_field, our_options[internal_field])
change_internal = True change_internal = True
@@ -187,4 +188,3 @@ class Command(BaseCommand):
print "Updated confirmation information in existing user's demographics." print "Updated confirmation information in existing user's demographics."
else: else:
print "No changes necessary to make to confirmation information in existing user's demographics." print "No changes necessary to make to confirmation information in existing user's demographics."

View File

@@ -49,7 +49,7 @@ class Command(BaseCommand):
# check additional required settings for import and export: # check additional required settings for import and export:
if options['mode'] in ('export', 'both'): if options['mode'] in ('export', 'both'):
for value in ['LOCAL_EXPORT','SFTP_EXPORT']: for value in ['LOCAL_EXPORT', 'SFTP_EXPORT']:
if value not in settings.PEARSON: if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings' raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value)) '(env/auth.json) for {0}'.format(value))
@@ -59,7 +59,7 @@ class Command(BaseCommand):
os.makedirs(source_dir) os.makedirs(source_dir)
if options['mode'] in ('import', 'both'): if options['mode'] in ('import', 'both'):
for value in ['LOCAL_IMPORT','SFTP_IMPORT']: for value in ['LOCAL_IMPORT', 'SFTP_IMPORT']:
if value not in settings.PEARSON: if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings' raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value)) '(env/auth.json) for {0}'.format(value))
@@ -135,17 +135,17 @@ class Command(BaseCommand):
k.set_contents_from_filename(source_file) k.set_contents_from_filename(source_file)
def export_pearson(): def export_pearson():
options = { 'dest-from-settings' : True } options = {'dest-from-settings': True}
call_command('pearson_export_cdd', **options) call_command('pearson_export_cdd', **options)
call_command('pearson_export_ead', **options) call_command('pearson_export_ead', **options)
mode = 'export' mode = 'export'
sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy = False) sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy=False)
s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True) s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True)
def import_pearson(): def import_pearson():
mode = 'import' mode = 'import'
try: try:
sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy = True) sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy=True)
s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False) s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False)
except Exception as e: except Exception as e:
dog_http_api.event('Pearson Import failure', str(e)) dog_http_api.event('Pearson Import failure', str(e))

View File

@@ -17,30 +17,31 @@ from student.models import User, TestCenterRegistration, TestCenterUser, get_tes
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def create_tc_user(username): def create_tc_user(username):
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass') user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
options = { options = {
'first_name' : 'TestFirst', 'first_name': 'TestFirst',
'last_name' : 'TestLast', 'last_name': 'TestLast',
'address_1' : 'Test Address', 'address_1': 'Test Address',
'city' : 'TestCity', 'city': 'TestCity',
'state' : 'Alberta', 'state': 'Alberta',
'postal_code' : 'A0B 1C2', 'postal_code': 'A0B 1C2',
'country' : 'CAN', 'country': 'CAN',
'phone' : '252-1866', 'phone': '252-1866',
'phone_country_code' : '1', 'phone_country_code': '1',
} }
call_command('pearson_make_tc_user', username, **options) call_command('pearson_make_tc_user', username, **options)
return TestCenterUser.objects.get(user=user) return TestCenterUser.objects.get(user=user)
def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code = 'exam1', accommodation_code = None): def create_tc_registration(username, course_id='org1/course1/term1', exam_code='exam1', accommodation_code=None):
options = { 'exam_series_code' : exam_code, options = {'exam_series_code': exam_code,
'eligibility_appointment_date_first' : '2013-01-01T00:00', 'eligibility_appointment_date_first': '2013-01-01T00:00',
'eligibility_appointment_date_last' : '2013-12-31T23:59', 'eligibility_appointment_date_last': '2013-12-31T23:59',
'accommodation_code' : accommodation_code, 'accommodation_code': accommodation_code,
'create_dummy_exam' : True, 'create_dummy_exam': True,
} }
call_command('pearson_make_tc_registration', username, course_id, **options) call_command('pearson_make_tc_registration', username, course_id, **options)
@@ -48,21 +49,23 @@ def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code
registrations = get_testcenter_registration(user, course_id, exam_code) registrations = get_testcenter_registration(user, course_id, exam_code)
return registrations[0] return registrations[0]
def create_multiple_registrations(prefix='test'): def create_multiple_registrations(prefix='test'):
username1 = '{}_multiple1'.format(prefix) username1 = '{}_multiple1'.format(prefix)
create_tc_user(username1) create_tc_user(username1)
create_tc_registration(username1) create_tc_registration(username1)
create_tc_registration(username1, course_id = 'org1/course2/term1') create_tc_registration(username1, course_id='org1/course2/term1')
create_tc_registration(username1, exam_code = 'exam2') create_tc_registration(username1, exam_code='exam2')
username2 = '{}_multiple2'.format(prefix) username2 = '{}_multiple2'.format(prefix)
create_tc_user(username2) create_tc_user(username2)
create_tc_registration(username2) create_tc_registration(username2)
username3 = '{}_multiple3'.format(prefix) username3 = '{}_multiple3'.format(prefix)
create_tc_user(username3) create_tc_user(username3)
create_tc_registration(username3, course_id = 'org1/course2/term1') create_tc_registration(username3, course_id='org1/course2/term1')
username4 = '{}_multiple4'.format(prefix) username4 = '{}_multiple4'.format(prefix)
create_tc_user(username4) create_tc_user(username4)
create_tc_registration(username4, exam_code = 'exam2') create_tc_registration(username4, exam_code='exam2')
def get_command_error_text(*args, **options): def get_command_error_text(*args, **options):
stderr_string = None stderr_string = None
@@ -90,6 +93,7 @@ def get_command_error_text(*args, **options):
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0])) raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
return stderr_string return stderr_string
def get_error_string_for_management_call(*args, **options): def get_error_string_for_management_call(*args, **options):
stdout_string = None stdout_string = None
old_stdout = sys.stdout old_stdout = sys.stdout
@@ -132,7 +136,8 @@ def get_file_info(dirpath):
numlines = len(filecontents) numlines = len(filecontents)
return filepath, numlines return filepath, numlines
else: else:
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath,filelist)) raise Exception("Expected to find a single file in {}, but found {}".format(dirpath, filelist))
class PearsonTestCase(TestCase): class PearsonTestCase(TestCase):
''' '''
@@ -159,6 +164,7 @@ class PearsonTestCase(TestCase):
# TestCenterUser.objects.all().delete() # TestCenterUser.objects.all().delete()
# TestCenterRegistration.objects.all().delete() # TestCenterRegistration.objects.all().delete()
class PearsonCommandTestCase(PearsonTestCase): class PearsonCommandTestCase(PearsonTestCase):
def test_missing_demographic_fields(self): def test_missing_demographic_fields(self):
@@ -199,8 +205,8 @@ class PearsonCommandTestCase(PearsonTestCase):
def test_export_single_cdd(self): def test_export_single_cdd(self):
# before we generate any tc_users, we expect there to be nothing to output: # before we generate any tc_users, we expect there to be nothing to output:
options = { 'dest-from-settings' : True } options = {'dest-from-settings': True}
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }): with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
call_command('pearson_export_cdd', **options) call_command('pearson_export_cdd', **options)
(filepath, numlines) = get_file_info(self.export_dir) (filepath, numlines) = get_file_info(self.export_dir)
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines") self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
@@ -221,7 +227,7 @@ class PearsonCommandTestCase(PearsonTestCase):
os.remove(filepath) os.remove(filepath)
# if we modify the record, then it should be output again: # if we modify the record, then it should be output again:
user_options = { 'first_name' : 'NewTestFirst', } user_options = {'first_name': 'NewTestFirst', }
call_command('pearson_make_tc_user', username, **user_options) call_command('pearson_make_tc_user', username, **user_options)
call_command('pearson_export_cdd', **options) call_command('pearson_export_cdd', **options)
(filepath, numlines) = get_file_info(self.export_dir) (filepath, numlines) = get_file_info(self.export_dir)
@@ -230,8 +236,8 @@ class PearsonCommandTestCase(PearsonTestCase):
def test_export_single_ead(self): def test_export_single_ead(self):
# before we generate any registrations, we expect there to be nothing to output: # before we generate any registrations, we expect there to be nothing to output:
options = { 'dest-from-settings' : True } options = {'dest-from-settings': True}
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }): with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
call_command('pearson_export_ead', **options) call_command('pearson_export_ead', **options)
(filepath, numlines) = get_file_info(self.export_dir) (filepath, numlines) = get_file_info(self.export_dir)
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines") self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
@@ -261,8 +267,8 @@ class PearsonCommandTestCase(PearsonTestCase):
def test_export_multiple(self): def test_export_multiple(self):
create_multiple_registrations("export") create_multiple_registrations("export")
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }): with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
options = { 'dest-from-settings' : True } options = {'dest-from-settings': True}
call_command('pearson_export_cdd', **options) call_command('pearson_export_cdd', **options)
(filepath, numlines) = get_file_info(self.export_dir) (filepath, numlines) = get_file_info(self.export_dir)
self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines)) self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
@@ -294,6 +300,7 @@ S3_BUCKET = 'edx-pearson-archive'
AWS_ACCESS_KEY_ID = 'put yours here' AWS_ACCESS_KEY_ID = 'put yours here'
AWS_SECRET_ACCESS_KEY = 'put yours here' AWS_SECRET_ACCESS_KEY = 'put yours here'
class PearsonTransferTestCase(PearsonTestCase): class PearsonTransferTestCase(PearsonTestCase):
''' '''
Class for tests running Pearson transfers Class for tests running Pearson transfers
@@ -302,14 +309,14 @@ class PearsonTransferTestCase(PearsonTestCase):
def test_transfer_config(self): def test_transfer_config(self):
with self.settings(DATADOG_API='FAKE_KEY'): with self.settings(DATADOG_API='FAKE_KEY'):
# TODO: why is this failing with the wrong error message?! # TODO: why is this failing with the wrong error message?!
stderrmsg = get_command_error_text('pearson_transfer', **{'mode' : 'garbage'}) stderrmsg = get_command_error_text('pearson_transfer', **{'mode': 'garbage'})
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries') self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
with self.settings(DATADOG_API='FAKE_KEY'): with self.settings(DATADOG_API='FAKE_KEY'):
stderrmsg = get_command_error_text('pearson_transfer') stderrmsg = get_command_error_text('pearson_transfer')
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries') self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
with self.settings(DATADOG_API='FAKE_KEY', with self.settings(DATADOG_API='FAKE_KEY',
PEARSON={'LOCAL_EXPORT' : self.export_dir, PEARSON={'LOCAL_EXPORT': self.export_dir,
'LOCAL_IMPORT' : self.import_dir }): 'LOCAL_IMPORT': self.import_dir}):
stderrmsg = get_command_error_text('pearson_transfer') stderrmsg = get_command_error_text('pearson_transfer')
self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings') self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
@@ -317,16 +324,16 @@ class PearsonTransferTestCase(PearsonTestCase):
raise SkipTest() raise SkipTest()
create_multiple_registrations('export_missing_dest') create_multiple_registrations('export_missing_dest')
with self.settings(DATADOG_API='FAKE_KEY', with self.settings(DATADOG_API='FAKE_KEY',
PEARSON={'LOCAL_EXPORT' : self.export_dir, PEARSON={'LOCAL_EXPORT': self.export_dir,
'SFTP_EXPORT' : 'this/does/not/exist', 'SFTP_EXPORT': 'this/does/not/exist',
'SFTP_HOSTNAME' : SFTP_HOSTNAME, 'SFTP_HOSTNAME': SFTP_HOSTNAME,
'SFTP_USERNAME' : SFTP_USERNAME, 'SFTP_USERNAME': SFTP_USERNAME,
'SFTP_PASSWORD' : SFTP_PASSWORD, 'SFTP_PASSWORD': SFTP_PASSWORD,
'S3_BUCKET' : S3_BUCKET, 'S3_BUCKET': S3_BUCKET,
}, },
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY): AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
options = { 'mode' : 'export'} options = {'mode': 'export'}
stderrmsg = get_command_error_text('pearson_transfer', **options) stderrmsg = get_command_error_text('pearson_transfer', **options)
self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist') self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
@@ -334,16 +341,16 @@ class PearsonTransferTestCase(PearsonTestCase):
raise SkipTest() raise SkipTest()
create_multiple_registrations("transfer_export") create_multiple_registrations("transfer_export")
with self.settings(DATADOG_API='FAKE_KEY', with self.settings(DATADOG_API='FAKE_KEY',
PEARSON={'LOCAL_EXPORT' : self.export_dir, PEARSON={'LOCAL_EXPORT': self.export_dir,
'SFTP_EXPORT' : 'results/topvue', 'SFTP_EXPORT': 'results/topvue',
'SFTP_HOSTNAME' : SFTP_HOSTNAME, 'SFTP_HOSTNAME': SFTP_HOSTNAME,
'SFTP_USERNAME' : SFTP_USERNAME, 'SFTP_USERNAME': SFTP_USERNAME,
'SFTP_PASSWORD' : SFTP_PASSWORD, 'SFTP_PASSWORD': SFTP_PASSWORD,
'S3_BUCKET' : S3_BUCKET, 'S3_BUCKET': S3_BUCKET,
}, },
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY): AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
options = { 'mode' : 'export'} options = {'mode': 'export'}
# call_command('pearson_transfer', **options) # call_command('pearson_transfer', **options)
# # confirm that the export directory is still empty: # # confirm that the export directory is still empty:
# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty") # self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
@@ -352,16 +359,16 @@ class PearsonTransferTestCase(PearsonTestCase):
raise SkipTest() raise SkipTest()
create_multiple_registrations('import_missing_src') create_multiple_registrations('import_missing_src')
with self.settings(DATADOG_API='FAKE_KEY', with self.settings(DATADOG_API='FAKE_KEY',
PEARSON={'LOCAL_IMPORT' : self.import_dir, PEARSON={'LOCAL_IMPORT': self.import_dir,
'SFTP_IMPORT' : 'this/does/not/exist', 'SFTP_IMPORT': 'this/does/not/exist',
'SFTP_HOSTNAME' : SFTP_HOSTNAME, 'SFTP_HOSTNAME': SFTP_HOSTNAME,
'SFTP_USERNAME' : SFTP_USERNAME, 'SFTP_USERNAME': SFTP_USERNAME,
'SFTP_PASSWORD' : SFTP_PASSWORD, 'SFTP_PASSWORD': SFTP_PASSWORD,
'S3_BUCKET' : S3_BUCKET, 'S3_BUCKET': S3_BUCKET,
}, },
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY): AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
options = { 'mode' : 'import'} options = {'mode': 'import'}
stderrmsg = get_command_error_text('pearson_transfer', **options) stderrmsg = get_command_error_text('pearson_transfer', **options)
self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist') self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
@@ -369,15 +376,15 @@ class PearsonTransferTestCase(PearsonTestCase):
raise SkipTest() raise SkipTest()
create_multiple_registrations('import_missing_src') create_multiple_registrations('import_missing_src')
with self.settings(DATADOG_API='FAKE_KEY', with self.settings(DATADOG_API='FAKE_KEY',
PEARSON={'LOCAL_IMPORT' : self.import_dir, PEARSON={'LOCAL_IMPORT': self.import_dir,
'SFTP_IMPORT' : 'results', 'SFTP_IMPORT': 'results',
'SFTP_HOSTNAME' : SFTP_HOSTNAME, 'SFTP_HOSTNAME': SFTP_HOSTNAME,
'SFTP_USERNAME' : SFTP_USERNAME, 'SFTP_USERNAME': SFTP_USERNAME,
'SFTP_PASSWORD' : SFTP_PASSWORD, 'SFTP_PASSWORD': SFTP_PASSWORD,
'S3_BUCKET' : S3_BUCKET, 'S3_BUCKET': S3_BUCKET,
}, },
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY): AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
options = { 'mode' : 'import'} options = {'mode': 'import'}
call_command('pearson_transfer', **options) call_command('pearson_transfer', **options)
self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty") self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")

View File

@@ -107,6 +107,7 @@ class UserProfile(models.Model):
TEST_CENTER_STATUS_ACCEPTED = "Accepted" TEST_CENTER_STATUS_ACCEPTED = "Accepted"
TEST_CENTER_STATUS_ERROR = "Error" TEST_CENTER_STATUS_ERROR = "Error"
class TestCenterUser(models.Model): class TestCenterUser(models.Model):
"""This is our representation of the User for in-person testing, and """This is our representation of the User for in-person testing, and
specifically for Pearson at this point. A few things to note: specifically for Pearson at this point. A few things to note:
@@ -190,7 +191,7 @@ class TestCenterUser(models.Model):
@staticmethod @staticmethod
def user_provided_fields(): def user_provided_fields():
return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', return ['first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country', 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name'] 'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
@@ -208,7 +209,7 @@ class TestCenterUser(models.Model):
@staticmethod @staticmethod
def _generate_edx_id(prefix): def _generate_edx_id(prefix):
NUM_DIGITS = 12 NUM_DIGITS = 12
return u"{}{:012}".format(prefix, randint(1, 10**NUM_DIGITS-1)) return u"{}{:012}".format(prefix, randint(1, 10 ** NUM_DIGITS - 1))
@staticmethod @staticmethod
def _generate_candidate_id(): def _generate_candidate_id():
@@ -237,10 +238,11 @@ class TestCenterUser(models.Model):
def is_pending(self): def is_pending(self):
return not self.is_accepted and not self.is_rejected return not self.is_accepted and not self.is_rejected
class TestCenterUserForm(ModelForm): class TestCenterUserForm(ModelForm):
class Meta: class Meta:
model = TestCenterUser model = TestCenterUser
fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation', fields = ('first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country', 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name') 'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
@@ -313,7 +315,8 @@ ACCOMMODATION_CODES = (
('SRSGNR', 'Separate Room and Sign Language Interpreter'), ('SRSGNR', 'Separate Room and Sign Language Interpreter'),
) )
ACCOMMODATION_CODE_DICT = { code : name for (code, name) in ACCOMMODATION_CODES } ACCOMMODATION_CODE_DICT = {code: name for (code, name) in ACCOMMODATION_CODES}
class TestCenterRegistration(models.Model): class TestCenterRegistration(models.Model):
""" """
@@ -417,7 +420,7 @@ class TestCenterRegistration(models.Model):
@classmethod @classmethod
def create(cls, testcenter_user, exam, accommodation_request): def create(cls, testcenter_user, exam, accommodation_request):
registration = cls(testcenter_user = testcenter_user) registration = cls(testcenter_user=testcenter_user)
registration.course_id = exam.course_id registration.course_id = exam.course_id
registration.accommodation_request = accommodation_request.strip() registration.accommodation_request = accommodation_request.strip()
registration.exam_series_code = exam.exam_series_code registration.exam_series_code = exam.exam_series_code
@@ -501,7 +504,7 @@ class TestCenterRegistration(models.Model):
return self.accommodation_code.split('*') return self.accommodation_code.split('*')
def get_accommodation_names(self): def get_accommodation_names(self):
return [ ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes() ] return [ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes()]
@property @property
def registration_signup_url(self): def registration_signup_url(self):
@@ -537,7 +540,7 @@ class TestCenterRegistration(models.Model):
class TestCenterRegistrationForm(ModelForm): class TestCenterRegistrationForm(ModelForm):
class Meta: class Meta:
model = TestCenterRegistration model = TestCenterRegistration
fields = ( 'accommodation_request', 'accommodation_code' ) fields = ('accommodation_request', 'accommodation_code')
def clean_accommodation_request(self): def clean_accommodation_request(self):
code = self.cleaned_data['accommodation_request'] code = self.cleaned_data['accommodation_request']
@@ -576,6 +579,7 @@ def get_testcenter_registration(user, course_id, exam_series_code):
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html) # Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
get_testcenter_registration.__test__ = False get_testcenter_registration.__test__ = False
def unique_id_for_user(user): def unique_id_for_user(user):
""" """
Return a unique id for a user, suitable for inserting into Return a unique id for a user, suitable for inserting into
@@ -666,6 +670,7 @@ class CourseEnrollmentAllowed(models.Model):
#### Helper methods for use from python manage.py shell and other classes. #### Helper methods for use from python manage.py shell and other classes.
def get_user_by_username_or_email(username_or_email): def get_user_by_username_or_email(username_or_email):
""" """
Return a User object, looking up by email if username_or_email contains a Return a User object, looking up by email if username_or_email contains a
@@ -767,4 +772,3 @@ def update_user_information(sender, instance, created, **kwargs):
log = logging.getLogger("mitx.discussion") log = logging.getLogger("mitx.discussion")
log.error(unicode(e)) log.error(unicode(e))
log.error("update user info to discussion failed for user with id: " + str(instance.id)) log.error("update user info to discussion failed for user with id: " + str(instance.id))

View File

@@ -17,6 +17,7 @@ COURSE_2 = 'edx/full/6.002_Spring_2012'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseEndingTest(TestCase): class CourseEndingTest(TestCase):
"""Test things related to course endings: certificates, surveys, etc""" """Test things related to course endings: certificates, surveys, etc"""
@@ -40,7 +41,7 @@ class CourseEndingTest(TestCase):
{'status': 'processing', {'status': 'processing',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': False,}) 'show_survey_button': False, })
cert_status = {'status': 'unavailable'} cert_status = {'status': 'unavailable'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),

View File

@@ -50,6 +50,7 @@ from statsd import statsd
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
def csrf_token(context): def csrf_token(context):
''' A csrf token that can be included in a form. ''' A csrf token that can be included in a form.
''' '''
@@ -73,8 +74,8 @@ def index(request, extra_context={}, user=None):
''' '''
# The course selection work is done in courseware.courses. # The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
if domain==False: # do explicit check, because domain=None is valid if domain == False: # do explicit check, because domain=None is valid
domain = request.META.get('HTTP_HOST') domain = request.META.get('HTTP_HOST')
courses = get_courses(None, domain=domain) courses = get_courses(None, domain=domain)
@@ -97,6 +98,7 @@ import re
day_pattern = re.compile('\s\d+,\s') day_pattern = re.compile('\s\d+,\s')
multimonth_pattern = re.compile('\s?\-\s?\S+\s') multimonth_pattern = re.compile('\s?\-\s?\S+\s')
def get_date_for_press(publish_date): def get_date_for_press(publish_date):
import datetime import datetime
# strip off extra months, and just use the first: # strip off extra months, and just use the first:
@@ -107,6 +109,7 @@ def get_date_for_press(publish_date):
date = datetime.datetime.strptime(date, "%B, %Y") date = datetime.datetime.strptime(date, "%B, %Y")
return date return date
def press(request): def press(request):
json_articles = cache.get("student_press_json_articles") json_articles = cache.get("student_press_json_articles")
if json_articles == None: if json_articles == None:
@@ -148,6 +151,7 @@ def cert_info(user, course):
return _cert_info(user, course, certificate_status_for_student(user, course.id)) return _cert_info(user, course, certificate_status_for_student(user, course.id))
def _cert_info(user, course, cert_status): def _cert_info(user, course, cert_status):
""" """
Implements the logic for cert_info -- split out for testing. Implements the logic for cert_info -- split out for testing.
@@ -175,7 +179,7 @@ def _cert_info(user, course, cert_status):
d = {'status': status, d = {'status': status,
'show_download_url': status == 'ready', 'show_download_url': status == 'ready',
'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):
@@ -204,6 +208,7 @@ def _cert_info(user, course, cert_status):
return d return d
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def dashboard(request): def dashboard(request):
@@ -237,9 +242,9 @@ def dashboard(request):
show_courseware_links_for = frozenset(course.id for course in courses show_courseware_links_for = frozenset(course.id for course in courses
if has_access(request.user, course, 'load')) if has_access(request.user, course, 'load'))
cert_statuses = { course.id: cert_info(request.user, course) for course in courses} cert_statuses = {course.id: cert_info(request.user, course) for course in courses}
exam_registrations = { course.id: exam_registration_info(request.user, course) for course in courses} exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
# Get the 3 most recent news # Get the 3 most recent news
top_news = _get_news(top=3) top_news = _get_news(top=3)
@@ -248,7 +253,7 @@ def dashboard(request):
'message': message, 'message': message,
'staff_access': staff_access, 'staff_access': staff_access,
'errored_courses': errored_courses, 'errored_courses': errored_courses,
'show_courseware_links_for' : show_courseware_links_for, 'show_courseware_links_for': show_courseware_links_for,
'cert_statuses': cert_statuses, 'cert_statuses': cert_statuses,
'news': top_news, 'news': top_news,
'exam_registrations': exam_registrations, 'exam_registrations': exam_registrations,
@@ -312,7 +317,7 @@ def change_enrollment(request):
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.display_name)}
org, course_num, run=course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
@@ -326,7 +331,7 @@ def change_enrollment(request):
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id) enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
enrollment.delete() enrollment.delete()
org, course_num, run=course_id.split("/") org, course_num, run = course_id.split("/")
statsd.increment("common.student.unenrollment", statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
@@ -345,7 +350,7 @@ def change_enrollment(request):
def accounts_login(request, error=""): def accounts_login(request, error=""):
return render_to_response('accounts_login.html', { 'error': error }) return render_to_response('accounts_login.html', {'error': error})
@@ -424,6 +429,7 @@ def change_setting(request):
return HttpResponse(json.dumps({'success': True, return HttpResponse(json.dumps({'success': True,
'location': up.location, })) 'location': up.location, }))
def _do_create_account(post_vars): def _do_create_account(post_vars):
""" """
Given cleaned post variables, create the User and UserProfile objects, as well as the Given cleaned post variables, create the User and UserProfile objects, as well as the
@@ -551,7 +557,7 @@ def create_account(request, post_override=None):
# Ok, looks like everything is legit. Create the account. # Ok, looks like everything is legit. Create the account.
ret = _do_create_account(post_vars) ret = _do_create_account(post_vars)
if isinstance(ret,HttpResponse): # if there was an error then return that if isinstance(ret, HttpResponse): # if there was an error then return that
return ret return ret
(user, profile, registration) = ret (user, profile, registration) = ret
@@ -591,7 +597,7 @@ def create_account(request, post_override=None):
eamap.user = login_user eamap.user = login_user
eamap.dtsignup = datetime.datetime.now() eamap.dtsignup = datetime.datetime.now()
eamap.save() eamap.save()
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'],eamap)) log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap))
if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
log.debug('bypassing activation email') log.debug('bypassing activation email')
@@ -603,6 +609,7 @@ def create_account(request, post_override=None):
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
def exam_registration_info(user, course): def exam_registration_info(user, course):
""" Returns a Registration object if the user is currently registered for a current """ Returns a Registration object if the user is currently registered for a current
exam of the course. Returns None if the user is not registered, or if there is no exam of the course. Returns None if the user is not registered, or if there is no
@@ -620,6 +627,7 @@ def exam_registration_info(user, course):
registration = None registration = None
return registration return registration
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def begin_exam_registration(request, course_id): def begin_exam_registration(request, course_id):
@@ -663,6 +671,7 @@ def begin_exam_registration(request, course_id):
return render_to_response('test_center_register.html', context) return render_to_response('test_center_register.html', context)
@ensure_csrf_cookie @ensure_csrf_cookie
def create_exam_registration(request, post_override=None): def create_exam_registration(request, post_override=None):
''' '''
@@ -725,7 +734,7 @@ def create_exam_registration(request, post_override=None):
# this registration screen. # this registration screen.
else: else:
accommodation_request = post_vars.get('accommodation_request','') accommodation_request = post_vars.get('accommodation_request', '')
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request) registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
needs_saving = True needs_saving = True
log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id)) log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id))
@@ -834,16 +843,17 @@ def password_reset(request):
form = PasswordResetForm(request.POST) form = PasswordResetForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save(use_https = request.is_secure(), form.save(use_https=request.is_secure(),
from_email = settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
request = request, request=request,
domain_override = request.get_host()) domain_override=request.get_host())
return HttpResponse(json.dumps({'success':True, return HttpResponse(json.dumps({'success': True,
'value': render_to_string('registration/password_reset_done.html', {})})) 'value': render_to_string('registration/password_reset_done.html', {})}))
else: else:
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'Invalid e-mail'})) 'error': 'Invalid e-mail'}))
@ensure_csrf_cookie @ensure_csrf_cookie
def reactivation_email(request): def reactivation_email(request):
''' Send an e-mail to reactivate a deactivated account, or to ''' Send an e-mail to reactivate a deactivated account, or to
@@ -856,6 +866,7 @@ def reactivation_email(request):
'error': 'No inactive user with this e-mail exists'})) 'error': 'No inactive user with this e-mail exists'}))
return reactivation_email_for_user(user) return reactivation_email_for_user(user)
def reactivation_email_for_user(user): def reactivation_email_for_user(user):
reg = Registration.objects.get(user=user) reg = Registration.objects.get(user=user)
@@ -996,11 +1007,11 @@ def pending_name_changes(request):
changes = list(PendingNameChange.objects.all()) changes = list(PendingNameChange.objects.all())
js = {'students': [{'new_name': c.new_name, js = {'students': [{'new_name': c.new_name,
'rationale':c.rationale, 'rationale': c.rationale,
'old_name':UserProfile.objects.get(user=c.user).name, 'old_name': UserProfile.objects.get(user=c.user).name,
'email':c.user.email, 'email': c.user.email,
'uid':c.user.id, 'uid': c.user.id,
'cid':c.id} for c in changes]} 'cid': c.id} for c in changes]}
return render_to_response('name_changes.html', js) return render_to_response('name_changes.html', js)
@@ -1057,6 +1068,8 @@ def accept_name_change(request):
# TODO: This is a giant kludge to give Pearson something to test against ASAP. # TODO: This is a giant kludge to give Pearson something to test against ASAP.
# Will need to get replaced by something that actually ties into TestCenterUser # Will need to get replaced by something that actually ties into TestCenterUser
@csrf_exempt @csrf_exempt
def test_center_login(request): def test_center_login(request):
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'): if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):

View File

@@ -2,21 +2,20 @@ from django.db import models
from django.db import models from django.db import models
class TrackingLog(models.Model): class TrackingLog(models.Model):
dtcreated = models.DateTimeField('creation date',auto_now_add=True) dtcreated = models.DateTimeField('creation date', auto_now_add=True)
username = models.CharField(max_length=32,blank=True) username = models.CharField(max_length=32, blank=True)
ip = models.CharField(max_length=32,blank=True) ip = models.CharField(max_length=32, blank=True)
event_source = models.CharField(max_length=32) event_source = models.CharField(max_length=32)
event_type = models.CharField(max_length=512,blank=True) event_type = models.CharField(max_length=512, blank=True)
event = models.TextField(blank=True) event = models.TextField(blank=True)
agent = models.CharField(max_length=256,blank=True) agent = models.CharField(max_length=256, blank=True)
page = models.CharField(max_length=512,blank=True,null=True) page = models.CharField(max_length=512, blank=True, null=True)
time = models.DateTimeField('event time') time = models.DateTimeField('event time')
host = models.CharField(max_length=64,blank=True) host = models.CharField(max_length=64, blank=True)
def __unicode__(self): def __unicode__(self):
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source, s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source,
self.event_type, self.page, self.event) self.event_type, self.page, self.event)
return s return s

View File

@@ -17,19 +17,21 @@ from track.models import TrackingLog
log = logging.getLogger("tracking") log = logging.getLogger("tracking")
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host'] LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', 'page', 'time', 'host']
def log_event(event): def log_event(event):
event_str = json.dumps(event) event_str = json.dumps(event)
log.info(event_str[:settings.TRACK_MAX_EVENT]) log.info(event_str[:settings.TRACK_MAX_EVENT])
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
event['time'] = dateutil.parser.parse(event['time']) event['time'] = dateutil.parser.parse(event['time'])
tldat = TrackingLog(**dict( (x,event[x]) for x in LOGFIELDS )) tldat = TrackingLog(**dict((x, event[x]) for x in LOGFIELDS))
try: try:
tldat.save() tldat.save()
except Exception as err: except Exception as err:
log.exception(err) log.exception(err)
def user_track(request): def user_track(request):
try: # TODO: Do the same for many of the optional META parameters try: # TODO: Do the same for many of the optional META parameters
username = request.user.username username = request.user.username
@@ -87,13 +89,14 @@ def server_track(request, event_type, event, page=None):
"host": request.META['SERVER_NAME'], "host": request.META['SERVER_NAME'],
} }
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
return return
log_event(event) log_event(event)
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def view_tracking_log(request,args=''): def view_tracking_log(request, args=''):
if not request.user.is_staff: if not request.user.is_staff:
return redirect('/') return redirect('/')
nlen = 100 nlen = 100
@@ -115,5 +118,4 @@ def view_tracking_log(request,args=''):
for rinst in record_instances: for rinst in record_instances:
rinst.dtstr = rinst.time.replace(tzinfo=pytz.utc).astimezone(pytz.timezone('US/Eastern')).strftime(fmt) rinst.dtstr = rinst.time.replace(tzinfo=pytz.utc).astimezone(pytz.timezone('US/Eastern')).strftime(fmt)
return render_to_response('tracking_log.html',{'records':record_instances}) return render_to_response('tracking_log.html', {'records': record_instances})

View File

@@ -58,4 +58,3 @@ def cache_if_anonymous(view_func):
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
return _decorated return _decorated

View File

@@ -1,7 +1,9 @@
import time, datetime import time
import datetime
import re import re
import calendar import calendar
def time_to_date(time_obj): def time_to_date(time_obj):
""" """
Convert a time.time_struct to a true universal time (can pass to js Date constructor) Convert a time.time_struct to a true universal time (can pass to js Date constructor)
@@ -9,6 +11,7 @@ def time_to_date(time_obj):
# TODO change to using the isoformat() function on datetime. js date can parse those # TODO change to using the isoformat() function on datetime. js date can parse those
return calendar.timegm(time_obj) * 1000 return calendar.timegm(time_obj) * 1000
def jsdate_to_time(field): def jsdate_to_time(field):
""" """
Convert a universal time (iso format) or msec since epoch to a time obj Convert a universal time (iso format) or msec since epoch to a time obj
@@ -16,7 +19,7 @@ def jsdate_to_time(field):
if field is None: if field is None:
return field return field
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple() return d.utctimetuple()
elif isinstance(field, int) or isinstance(field, float): elif isinstance(field, int) or isinstance(field, float):
return time.gmtime(field / 1000) return time.gmtime(field / 1000)

View File

@@ -13,7 +13,7 @@ def expect_json(view_function):
def expect_json_with_cloned_request(request, *args, **kwargs): def expect_json_with_cloned_request(request, *args, **kwargs):
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information # cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
# e.g. 'charset', so we can't do a direct string compare # e.g. 'charset', so we can't do a direct string compare
if request.META.get('CONTENT_TYPE','').lower().startswith("application/json"): if request.META.get('CONTENT_TYPE', '').lower().startswith("application/json"):
cloned_request = copy.copy(request) cloned_request = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy() cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.body)) cloned_request.POST.update(json.loads(request.body))

View File

@@ -93,6 +93,7 @@ def accepts(request, media_type):
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", "")) accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
return media_type in [t for (t, p, q) in accept] return media_type in [t for (t, p, q) in accept]
def debug_request(request): def debug_request(request):
"""Return a pretty printed version of the request""" """Return a pretty printed version of the request"""

View File

@@ -12,6 +12,7 @@ from xmodule.vertical_module import VerticalModule
log = logging.getLogger("mitx.xmodule_modifiers") log = logging.getLogger("mitx.xmodule_modifiers")
def wrap_xmodule(get_html, module, template, context=None): def wrap_xmodule(get_html, module, template, context=None):
""" """
Wraps the results of get_html in a standard <section> with identifying Wraps the results of get_html in a standard <section> with identifying
@@ -32,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), 'content': get_html(),
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None, 'display_name': module.metadata.get('display_name') if module.metadata is not None else None,
'class_': module.__class__.__name__, 'class_': module.__class__.__name__,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
@@ -52,6 +53,7 @@ def replace_course_urls(get_html, course_id):
return static_replace.replace_course_urls(get_html(), course_id) return static_replace.replace_course_urls(get_html(), course_id)
return _get_html return _get_html
def replace_static_urls(get_html, data_dir, course_namespace=None): def replace_static_urls(get_html, data_dir, course_namespace=None):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
@@ -64,6 +66,7 @@ def replace_static_urls(get_html, data_dir, course_namespace=None):
return static_replace.replace_static_urls(get_html(), data_dir, course_namespace) return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
return _get_html return _get_html
def grade_histogram(module_id): def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem. ''' Print out a histogram of grades on a given problem.
Part of staff member debug info. Part of staff member debug info.
@@ -98,7 +101,7 @@ def add_histogram(get_html, module, user):
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
return get_html() return get_html()
module_id = module.id module_id = module.id
@@ -114,35 +117,35 @@ def add_histogram(get_html, module, user):
# doesn't like symlinks) # doesn't like symlinks)
filepath = filename filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1] data_dir = osfs.root_path.rsplit('/')[-1]
giturl = module.metadata.get('giturl','https://github.com/MITx') giturl = module.metadata.get('giturl', 'https://github.com/MITx')
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath) edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
else: else:
edit_link = False edit_link = False
# Need to define all the variables that are about to be used # Need to define all the variables that are about to be used
giturl = "" giturl = ""
data_dir = "" data_dir = ""
source_file = module.metadata.get('source_file','') # source used to generate the problem XML, eg latex or word source_file = module.metadata.get('source_file', '') # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not # useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime() now = time.gmtime()
is_released = "unknown" is_released = "unknown"
mstart = getattr(module.descriptor,'start') mstart = getattr(module.descriptor, 'start')
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'definition': module.definition.get('data'), staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4), 'metadata': json.dumps(module.metadata, indent=4),
'location': module.location, 'location': module.location,
'xqa_key': module.metadata.get('xqa_key',''), 'xqa_key': module.metadata.get('xqa_key', ''),
'source_file' : source_file, 'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file), 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__), 'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes # Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-','_'), 'element_id': module.location.html_id().replace('-', '_'),
'edit_link': edit_link, 'edit_link': edit_link,
'user': user, 'user': user,
'xqa_server' : settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa'), 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram), 'histogram': json.dumps(histogram),
'render_histogram': render_histogram, 'render_histogram': render_histogram,
'module_content': get_html(), 'module_content': get_html(),
@@ -151,4 +154,3 @@ def add_histogram(get_html, module, user):
return render_to_string("staff_problem_info.html", staff_context) return render_to_string("staff_problem_info.html", staff_context)
return _get_html return _get_html

View File

@@ -121,9 +121,9 @@ def evaluator(variables, functions, string, cs=False):
# confusing. They may also conflict with variables if we ever allow e.g. # confusing. They may also conflict with variables if we ever allow e.g.
# 5R instead of 5*R # 5R instead of 5*R
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
def super_float(text): def super_float(text):
''' Like float, but with si extensions. 1k goes to 1000''' ''' Like float, but with si extensions. 1k goes to 1000'''

View File

@@ -75,7 +75,7 @@ global_context = {'random': random,
'draganddrop': verifiers.draganddrop} 'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"] html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
@@ -453,7 +453,7 @@ class LoncapaProblem(object):
exec code in context, context exec code in context, context
except Exception as err: except Exception as err:
log.exception("Error while execing script code: " + code) log.exception("Error while execing script code: " + code)
msg = "Error while executing script code: %s" % str(err).replace('<','&lt;') msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
raise responsetypes.LoncapaProblemError(msg) raise responsetypes.LoncapaProblemError(msg)
finally: finally:
sys.path = original_path sys.path = original_path
@@ -502,7 +502,7 @@ class LoncapaProblem(object):
'id': problemtree.get('id'), 'id': problemtree.get('id'),
'feedback': {'message': msg, 'feedback': {'message': msg,
'hint': hint, 'hint': hint,
'hintmode': hintmode,}} 'hintmode': hintmode, }}
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag) input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state) the_input = input_type_cls(self.system, problemtree, state)

View File

@@ -1 +0,0 @@

View File

@@ -17,17 +17,17 @@ from nltk.tree import Tree
ARROWS = ('<->', '->') ARROWS = ('<->', '->')
## Defines a simple pyparsing tokenizer for chemical equations ## Defines a simple pyparsing tokenizer for chemical equations
elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be', elements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be',
'Bh','Bi','Bk','Br','C','Ca','Cd','Ce','Cf','Cl','Cm', 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm',
'Cn','Co','Cr','Cs','Cu','Db','Ds','Dy','Er','Es','Eu', 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu',
'F','Fe','Fl','Fm','Fr','Ga','Gd','Ge','H','He','Hf', 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf',
'Hg','Ho','Hs','I','In','Ir','K','Kr','La','Li','Lr', 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr',
'Lu','Lv','Md','Mg','Mn','Mo','Mt','N','Na','Nb','Nd', 'Lu', 'Lv', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd',
'Ne','Ni','No','Np','O','Os','P','Pa','Pb','Pd','Pm', 'Ne', 'Ni', 'No', 'Np', 'O', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm',
'Po','Pr','Pt','Pu','Ra','Rb','Re','Rf','Rg','Rh','Rn', 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn',
'Ru','S','Sb','Sc','Se','Sg','Si','Sm','Sn','Sr','Ta', 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta',
'Tb','Tc','Te','Th','Ti','Tl','Tm','U','Uuo','Uup', 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'U', 'Uuo', 'Uup',
'Uus','Uut','V','W','Xe','Y','Yb','Zn','Zr'] 'Uus', 'Uut', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']
digits = map(str, range(10)) digits = map(str, range(10))
symbols = list("[](){}^+-/") symbols = list("[](){}^+-/")
phases = ["(s)", "(l)", "(g)", "(aq)"] phases = ["(s)", "(l)", "(g)", "(aq)"]
@@ -252,7 +252,7 @@ def _get_final_tree(s):
''' '''
tokenized = tokenizer.parseString(s) tokenized = tokenizer.parseString(s)
parsed = parser.parse(tokenized) parsed = parser.parse(tokenized)
merged = _merge_children(parsed, {'S','group'}) merged = _merge_children(parsed, {'S', 'group'})
final = _clean_parse_tree(merged) final = _clean_parse_tree(merged)
return final return final

View File

@@ -3,6 +3,7 @@
# #
# Used by responsetypes and capa_problem # Used by responsetypes and capa_problem
class CorrectMap(object): class CorrectMap(object):
""" """
Stores map between answer_id and response evaluation result for each question Stores map between answer_id and response evaluation result for each question
@@ -152,6 +153,3 @@ class CorrectMap(object):
if not isinstance(other_cmap, CorrectMap): if not isinstance(other_cmap, CorrectMap):
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap) raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
self.cmap.update(other_cmap.get_dict()) self.cmap.update(other_cmap.get_dict())

View File

@@ -22,6 +22,8 @@ log = logging.getLogger('mitx.' + __name__)
registry = TagRegistry() registry = TagRegistry()
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class MathRenderer(object): class MathRenderer(object):
tags = ['math'] tags = ['math']
@@ -77,6 +79,7 @@ registry.register(MathRenderer)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class SolutionRenderer(object): class SolutionRenderer(object):
''' '''
A solution is just a <span>...</span> which is given an ID, that is used for displaying an A solution is just a <span>...</span> which is given an ID, that is used for displaying an
@@ -97,4 +100,3 @@ class SolutionRenderer(object):
return etree.XML(html) return etree.XML(html)
registry.register(SolutionRenderer) registry.register(SolutionRenderer)

View File

@@ -54,6 +54,7 @@ log = logging.getLogger('mitx.' + __name__)
registry = TagRegistry() registry = TagRegistry()
class Attribute(object): class Attribute(object):
""" """
Allows specifying required and optional attributes for input types. Allows specifying required and optional attributes for input types.
@@ -413,7 +414,7 @@ class JavascriptInput(InputTypeBase):
return [Attribute('params', None), return [Attribute('params', None),
Attribute('problem_state', None), Attribute('problem_state', None),
Attribute('display_class', None), Attribute('display_class', None),
Attribute('display_file', None),] Attribute('display_file', None), ]
def setup(self): def setup(self):
@@ -477,12 +478,13 @@ class TextLine(InputTypeBase):
def _extra_context(self): def _extra_context(self):
return {'do_math': self.do_math, return {'do_math': self.do_math,
'preprocessor': self.preprocessor,} 'preprocessor': self.preprocessor, }
registry.register(TextLine) registry.register(TextLine)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class FileSubmission(InputTypeBase): class FileSubmission(InputTypeBase):
""" """
Upload some files (e.g. for programming assignments) Upload some files (e.g. for programming assignments)
@@ -508,7 +510,7 @@ class FileSubmission(InputTypeBase):
Convert the list of allowed files to a convenient format. Convert the list of allowed files to a convenient format.
""" """
return [Attribute('allowed_files', '[]', transform=cls.parse_files), return [Attribute('allowed_files', '[]', transform=cls.parse_files),
Attribute('required_files', '[]', transform=cls.parse_files),] Attribute('required_files', '[]', transform=cls.parse_files), ]
def setup(self): def setup(self):
""" """
@@ -524,7 +526,7 @@ class FileSubmission(InputTypeBase):
self.msg = FileSubmission.submitted_msg self.msg = FileSubmission.submitted_msg
def _extra_context(self): def _extra_context(self):
return {'queue_len': self.queue_len,} return {'queue_len': self.queue_len, }
return context return context
registry.register(FileSubmission) registry.register(FileSubmission)
@@ -582,7 +584,7 @@ class CodeInput(InputTypeBase):
def _extra_context(self): def _extra_context(self):
"""Defined queue_len, add it """ """Defined queue_len, add it """
return {'queue_len': self.queue_len,} return {'queue_len': self.queue_len, }
registry.register(CodeInput) registry.register(CodeInput)
@@ -606,7 +608,7 @@ class Schematic(InputTypeBase):
Attribute('parts', None), Attribute('parts', None),
Attribute('analyses', None), Attribute('analyses', None),
Attribute('initial_value', None), Attribute('initial_value', None),
Attribute('submit_analyses', None),] Attribute('submit_analyses', None), ]
return context return context
@@ -614,6 +616,7 @@ registry.register(Schematic)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class ImageInput(InputTypeBase): class ImageInput(InputTypeBase):
""" """
Clickable image as an input field. Element should specify the image source, height, Clickable image as an input field. Element should specify the image source, height,
@@ -635,7 +638,7 @@ class ImageInput(InputTypeBase):
""" """
return [Attribute('src'), return [Attribute('src'),
Attribute('height'), Attribute('height'),
Attribute('width'),] Attribute('width'), ]
def setup(self): def setup(self):
@@ -660,6 +663,7 @@ registry.register(ImageInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class Crystallography(InputTypeBase): class Crystallography(InputTypeBase):
""" """
An input for crystallography -- user selects 3 points on the axes, and we get a plane. An input for crystallography -- user selects 3 points on the axes, and we get a plane.
@@ -728,18 +732,19 @@ class ChemicalEquationInput(InputTypeBase):
""" """
Can set size of text field. Can set size of text field.
""" """
return [Attribute('size', '20'),] return [Attribute('size', '20'), ]
def _extra_context(self): def _extra_context(self):
""" """
TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded. TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded.
""" """
return {'previewer': '/static/js/capa/chemical_equation_preview.js',} return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
registry.register(ChemicalEquationInput) registry.register(ChemicalEquationInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class DragAndDropInput(InputTypeBase): class DragAndDropInput(InputTypeBase):
""" """
Input for drag and drop problems. Allows student to drag and drop images and Input for drag and drop problems. Allows student to drag and drop images and

View File

@@ -186,8 +186,8 @@ class LoncapaResponse(object):
tree = etree.Element('span') tree = etree.Element('span')
# problem author can make this span display:inline # problem author can make this span display:inline
if self.xml.get('inline',''): if self.xml.get('inline', ''):
tree.set('class','inline') tree.set('class', 'inline')
for item in self.xml: for item in self.xml:
# call provided procedure to do the rendering # call provided procedure to do the rendering
@@ -1294,7 +1294,7 @@ class CodeResponse(LoncapaResponse):
# State associated with the queueing request # State associated with the queueing request
queuestate = {'key': queuekey, queuestate = {'key': queuekey,
'time': qtime,} 'time': qtime, }
cmap = CorrectMap() cmap = CorrectMap()
if error: if error:

View File

@@ -8,6 +8,7 @@ import xml.sax.saxutils as saxutils
TEST_DIR = os.path.dirname(os.path.realpath(__file__)) TEST_DIR = os.path.dirname(os.path.realpath(__file__))
def tst_render_template(template, context): def tst_render_template(template, context):
""" """
A test version of render to template. Renders to the repr of the context, completely ignoring A test version of render to template. Renders to the repr of the context, completely ignoring
@@ -25,7 +26,7 @@ test_system = Mock(
user=Mock(), user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True, debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10}, xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id = 'student' anonymous_student_id='student'
) )

View File

@@ -8,6 +8,7 @@ from capa import customrender
# just a handy shortcut # just a handy shortcut
lookup_tag = customrender.registry.get_class_for_tag lookup_tag = customrender.registry.get_class_for_tag
def extract_context(xml): def extract_context(xml):
""" """
Given an xml element corresponding to the output of test_system.render_template, get back the Given an xml element corresponding to the output of test_system.render_template, get back the
@@ -15,9 +16,11 @@ def extract_context(xml):
""" """
return eval(xml.text) return eval(xml.text)
def quote_attr(s): def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class HelperTest(unittest.TestCase): class HelperTest(unittest.TestCase):
''' '''
Make sure that our helper function works! Make sure that our helper function works!
@@ -50,7 +53,7 @@ class SolutionRenderTest(unittest.TestCase):
# our test_system "renders" templates to a div with the repr of the context # our test_system "renders" templates to a div with the repr of the context
xml = renderer.get_html() xml = renderer.get_html()
context = extract_context(xml) context = extract_context(xml)
self.assertEqual(context, {'id' : 'solution_12'}) self.assertEqual(context, {'id': 'solution_12'})
class MathRenderTest(unittest.TestCase): class MathRenderTest(unittest.TestCase):
@@ -73,4 +76,3 @@ class MathRenderTest(unittest.TestCase):
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing. # NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.

View File

@@ -31,6 +31,7 @@ lookup_tag = inputtypes.registry.get_class_for_tag
def quote_attr(s): def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class OptionInputTest(unittest.TestCase): class OptionInputTest(unittest.TestCase):
''' '''
Make sure option inputs work Make sure option inputs work
@@ -100,7 +101,7 @@ class ChoiceGroupTest(unittest.TestCase):
'input_type': expected_input_type, 'input_type': expected_input_type,
'choices': [('foil1', '<text>This is foil One.</text>'), 'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'), ('foil2', '<text>This is foil Two.</text>'),
('foil3', 'This is foil Three.'),], ('foil3', 'This is foil Three.'), ],
'name_array_suffix': expected_suffix, # what is this for?? 'name_array_suffix': expected_suffix, # what is this for??
} }
@@ -137,7 +138,7 @@ class JavascriptInputTest(unittest.TestCase):
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
state = {'value': '3',} state = {'value': '3', }
the_input = lookup_tag('javascriptinput')(test_system, element, state) the_input = lookup_tag('javascriptinput')(test_system, element, state)
context = the_input._get_render_context() context = the_input._get_render_context()
@@ -149,7 +150,7 @@ class JavascriptInputTest(unittest.TestCase):
'params': params, 'params': params,
'display_file': display_file, 'display_file': display_file,
'display_class': display_class, 'display_class': display_class,
'problem_state': problem_state,} 'problem_state': problem_state, }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -165,7 +166,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',} state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state) the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context() context = the_input._get_render_context()
@@ -193,7 +194,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',} state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state) the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context() context = the_input._get_render_context()
@@ -231,7 +232,7 @@ class FileSubmissionTest(unittest.TestCase):
state = {'value': 'BumbleBee.py', state = {'value': 'BumbleBee.py',
'status': 'incomplete', 'status': 'incomplete',
'feedback' : {'message': '3'}, } 'feedback': {'message': '3'}, }
input_class = lookup_tag('filesubmission') input_class = lookup_tag('filesubmission')
the_input = input_class(test_system, element, state) the_input = input_class(test_system, element, state)
@@ -275,7 +276,7 @@ class CodeInputTest(unittest.TestCase):
state = {'value': 'print "good evening"', state = {'value': 'print "good evening"',
'status': 'incomplete', 'status': 'incomplete',
'feedback' : {'message': '3'}, } 'feedback': {'message': '3'}, }
input_class = lookup_tag('codeinput') input_class = lookup_tag('codeinput')
the_input = input_class(test_system, element, state) the_input = input_class(test_system, element, state)
@@ -488,7 +489,7 @@ class ChemicalEquationTest(unittest.TestCase):
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah',} state = {'value': 'H2OYeah', }
the_input = lookup_tag('chemicalequationinput')(test_system, element, state) the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
context = the_input._get_render_context() context = the_input._get_render_context()

View File

@@ -16,6 +16,7 @@ from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
class MultiChoiceTest(unittest.TestCase): class MultiChoiceTest(unittest.TestCase):
def test_MC_grade(self): def test_MC_grade(self):
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml" multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
@@ -295,16 +296,16 @@ class CodeResponseTest(unittest.TestCase):
old_cmap = CorrectMap() old_cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids): for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i queuekey = 1000 + i
queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now()) queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
# Message format common to external graders # Message format common to external graders
grader_msg = '<span>MESSAGE</span>' # Must be valid XML grader_msg = '<span>MESSAGE</span>' # Must be valid XML
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg': grader_msg}) correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg': grader_msg}) incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
xserver_msgs = {'correct': correct_score_msg, xserver_msgs = {'correct': correct_score_msg,
'incorrect': incorrect_score_msg,} 'incorrect': incorrect_score_msg, }
# Incorrect queuekey, state should not be updated # Incorrect queuekey, state should not be updated
for correctness in ['correct', 'incorrect']: for correctness in ['correct', 'incorrect']:
@@ -325,7 +326,7 @@ class CodeResponseTest(unittest.TestCase):
new_cmap = CorrectMap() new_cmap = CorrectMap()
new_cmap.update(old_cmap) new_cmap.update(old_cmap)
npoints = 1 if correctness=='correct' else 0 npoints = 1 if correctness == 'correct' else 0
new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None) new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None)
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
@@ -361,7 +362,7 @@ class CodeResponseTest(unittest.TestCase):
for i, answer_id in enumerate(answer_ids): for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i queuekey = 1000 + i
latest_timestamp = datetime.now() latest_timestamp = datetime.now()
queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp) queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate)) cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
test_lcp.correct_map.update(cmap) test_lcp.correct_map.update(cmap)
@@ -412,6 +413,7 @@ class ChoiceResponseTest(unittest.TestCase):
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
class JavascriptResponseTest(unittest.TestCase): class JavascriptResponseTest(unittest.TestCase):
def test_jr_grade(self): def test_jr_grade(self):
@@ -424,4 +426,3 @@ class JavascriptResponseTest(unittest.TestCase):
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect') self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')

View File

@@ -51,15 +51,17 @@ def convert_files_to_filenames(answers):
new_answers = dict() new_answers = dict()
for answer_id in answers.keys(): for answer_id in answers.keys():
answer = answers[answer_id] answer = answers[answer_id]
if is_list_of_files(answer): # Files are stored as a list, even if one file if is_list_of_files(answer): # Files are stored as a list, even if one file
new_answers[answer_id] = [f.name for f in answer] new_answers[answer_id] = [f.name for f in answer]
else: else:
new_answers[answer_id] = answers[answer_id] new_answers[answer_id] = answers[answer_id]
return new_answers return new_answers
def is_list_of_files(files): def is_list_of_files(files):
return isinstance(files, list) and all(is_file(f) for f in files) return isinstance(files, list) and all(is_file(f) for f in files)
def is_file(file_to_test): def is_file(file_to_test):
''' '''
Duck typing to check if 'file_to_test' is a File object Duck typing to check if 'file_to_test' is a File object
@@ -86,4 +88,3 @@ def find_with_default(node, path, default):
return v.text return v.text
else: else:
return default return default

View File

@@ -10,6 +10,7 @@ import requests
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
dateformat = '%Y%m%d%H%M%S' dateformat = '%Y%m%d%H%M%S'
def make_hashkey(seed): def make_hashkey(seed):
''' '''
Generate a string key by hashing Generate a string key by hashing
@@ -29,9 +30,9 @@ def make_xheader(lms_callback_url, lms_key, queue_name):
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string) 'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
} }
""" """
return json.dumps({ 'lms_callback_url': lms_callback_url, return json.dumps({'lms_callback_url': lms_callback_url,
'lms_key': lms_key, 'lms_key': lms_key,
'queue_name': queue_name }) 'queue_name': queue_name})
def parse_xreply(xreply): def parse_xreply(xreply):
@@ -96,18 +97,18 @@ class XQueueInterface(object):
def _login(self): def _login(self):
payload = { 'username': self.auth['username'], payload = {'username': self.auth['username'],
'password': self.auth['password'] } 'password': self.auth['password']}
return self._http_post(self.url + '/xqueue/login/', payload) return self._http_post(self.url + '/xqueue/login/', payload)
def _send_to_queue(self, header, body, files_to_upload): def _send_to_queue(self, header, body, files_to_upload):
payload = {'xqueue_header': header, payload = {'xqueue_header': header,
'xqueue_body' : body} 'xqueue_body': body}
files = {} files = {}
if files_to_upload is not None: if files_to_upload is not None:
for f in files_to_upload: for f in files_to_upload:
files.update({ f.name: f }) files.update({f.name: f})
return self._http_post(self.url + '/xqueue/submit/', payload, files=files) return self._http_post(self.url + '/xqueue/submit/', payload, files=files)

View File

@@ -3,7 +3,8 @@ A handy util to print a django-debug-screen-like stack trace with
values of local variables. values of local variables.
""" """
import sys, traceback import sys
import traceback
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
@@ -48,5 +49,3 @@ def supertrace(max_len=160):
print s print s
except: except:
print "<ERROR WHILE PRINTING VALUE>" print "<ERROR WHILE PRINTING VALUE>"

View File

@@ -29,6 +29,7 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
# Generated this many different variants of problems with rerandomize=per_student # Generated this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20 NUM_RANDOMIZATION_BINS = 20
def randomization_bin(seed, problem_id): def randomization_bin(seed, problem_id):
""" """
Pick a randomization bin for the problem given the user's seed and a problem id. Pick a randomization bin for the problem given the user's seed and a problem id.
@@ -43,6 +44,7 @@ def randomization_bin(seed, problem_id):
# get the first few digits of the hash, convert to an int, then mod. # get the first few digits of the hash, convert to an int, then mod.
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
def only_one(lst, default="", process=lambda x: x): def only_one(lst, default="", process=lambda x: x):
""" """
If lst is empty, returns default If lst is empty, returns default
@@ -283,7 +285,7 @@ class CapaModule(XModule):
# Next, generate a fresh LoncapaProblem # Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa state=None, # Tabula rasa
seed=self.seed, system=self.system) seed=self.seed, system=self.system)
# Prepend a scary warning to the student # Prepend a scary warning to the student
@@ -302,7 +304,7 @@ class CapaModule(XModule):
html = warning html = warning
try: try:
html += self.lcp.get_html() html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up except Exception, err: # Couldn't do it. Give up
log.exception(err) log.exception(err)
raise raise
@@ -315,7 +317,7 @@ class CapaModule(XModule):
# check button is context-specific. # check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left # Put a "Check" button if unlimited attempts or still some left
if self.max_attempts is None or self.attempts < self.max_attempts-1: if self.max_attempts is None or self.attempts < self.max_attempts - 1:
check_button = "Check" check_button = "Check"
else: else:
# Will be final check so let user know that # Will be final check so let user know that
@@ -561,9 +563,9 @@ class CapaModule(XModule):
current_time = datetime.datetime.now() current_time = datetime.datetime.now()
prev_submit_time = self.lcp.get_recentmost_queuetime() prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime'] waittime_between_requests = self.system.xqueue['waittime']
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try: try:
old_state = self.lcp.get_state() old_state = self.lcp.get_state()
@@ -596,7 +598,7 @@ class CapaModule(XModule):
event_info['attempts'] = self.attempts event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info) self.system.track_function('save_problem_check', event_info)
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state()) self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML # render problem into HTML
@@ -707,7 +709,7 @@ class CapaDescriptor(RawDescriptor):
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor""" """Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor,self).editable_metadata_fields subset = super(CapaDescriptor, self).editable_metadata_fields
if 'markdown' in subset: if 'markdown' in subset:
subset.remove('markdown') subset.remove('markdown')
return subset return subset

View File

@@ -50,14 +50,16 @@ ACCEPT_FILE_UPLOAD = False
TRUE_DICT = ["True", True, "TRUE", "true"] TRUE_DICT = ["True", True, "TRUE", "true"]
HUMAN_TASK_TYPE = { HUMAN_TASK_TYPE = {
'selfassessment' : "Self Assessment", 'selfassessment': "Self Assessment",
'openended' : "External Grader", 'openended': "External Grader",
} }
class IncorrectMaxScoreError(Exception): class IncorrectMaxScoreError(Exception):
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg
class CombinedOpenEndedModule(XModule): class CombinedOpenEndedModule(XModule):
""" """
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).

View File

@@ -1,12 +1,14 @@
import logging import logging
from lxml import etree from lxml import etree
log=logging.getLogger(__name__) log = logging.getLogger(__name__)
class RubricParsingError(Exception): class RubricParsingError(Exception):
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg
class CombinedOpenEndedRubric(object): class CombinedOpenEndedRubric(object):
def __init__ (self, system, view_only = False): def __init__ (self, system, view_only = False):
@@ -28,7 +30,7 @@ class CombinedOpenEndedRubric(object):
try: try:
rubric_categories = self.extract_categories(rubric_xml) rubric_categories = self.extract_categories(rubric_xml)
html = self.system.render_template('open_ended_rubric.html', html = self.system.render_template('open_ended_rubric.html',
{'categories' : rubric_categories, {'categories': rubric_categories,
'has_score': self.has_score, 'has_score': self.has_score,
'view_only': self.view_only}) 'view_only': self.view_only})
success = True success = True

View File

@@ -9,6 +9,7 @@ from pkg_resources import resource_string
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
class ConditionalModule(XModule): class ConditionalModule(XModule):
''' '''
Blocks child module from showing unless certain conditions are met. Blocks child module from showing unless certain conditions are met.
@@ -43,7 +44,7 @@ class ConditionalModule(XModule):
""" """
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
self.contents = None self.contents = None
self.condition = self.metadata.get('condition','') self.condition = self.metadata.get('condition', '')
#log.debug('conditional module required=%s' % self.required_modules_list) #log.debug('conditional module required=%s' % self.required_modules_list)
def _get_required_modules(self): def _get_required_modules(self):
@@ -56,7 +57,7 @@ class ConditionalModule(XModule):
def is_condition_satisfied(self): def is_condition_satisfied(self):
self._get_required_modules() self._get_required_modules()
if self.condition=='require_completed': if self.condition == 'require_completed':
# all required modules must be completed, as determined by # all required modules must be completed, as determined by
# the modules .is_completed() method # the modules .is_completed() method
for module in self.required_modules: for module in self.required_modules:
@@ -70,7 +71,7 @@ class ConditionalModule(XModule):
else: else:
log.debug('conditional module: %s IS completed' % module) log.debug('conditional module: %s IS completed' % module)
return True return True
elif self.condition=='require_attempted': elif self.condition == 'require_attempted':
# all required modules must be attempted, as determined by # all required modules must be attempted, as determined by
# the modules .is_attempted() method # the modules .is_attempted() method
for module in self.required_modules: for module in self.required_modules:
@@ -114,6 +115,7 @@ class ConditionalModule(XModule):
return json.dumps({'html': html}) return json.dumps({'html': html})
class ConditionalDescriptor(SequenceDescriptor): class ConditionalDescriptor(SequenceDescriptor):
module_class = ConditionalModule module_class = ConditionalModule
@@ -125,7 +127,7 @@ class ConditionalDescriptor(SequenceDescriptor):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ConditionalDescriptor, self).__init__(*args, **kwargs) super(ConditionalDescriptor, self).__init__(*args, **kwargs)
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')] required_module_list = [tuple(x.split('/', 1)) for x in self.metadata.get('required', '').split('&')]
self.required_module_locations = [] self.required_module_locations = []
for (tag, name) in required_module_list: for (tag, name) in required_module_list:
loc = self.location.dict() loc = self.location.dict()
@@ -138,4 +140,3 @@ class ConditionalDescriptor(SequenceDescriptor):
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are """Returns a list of XModuleDescritpor instances upon which this module depends, but are
not children of this module""" not children of this module"""
return [self.system.load_item(loc) for loc in self.required_module_locations] return [self.system.load_item(loc) for loc in self.required_module_locations]

View File

@@ -11,10 +11,11 @@ from xmodule.modulestore import Location
from .django import contentstore from .django import contentstore
from PIL import Image from PIL import Image
class StaticContent(object): class StaticContent(object):
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None): def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None):
self.location = loc self.location = loc
self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
self.content_type = content_type self.content_type = content_type
self.data = data self.data = data
self.last_modified_at = last_modified_at self.last_modified_at = last_modified_at
@@ -29,7 +30,7 @@ class StaticContent(object):
@staticmethod @staticmethod
def generate_thumbnail_name(original_name): def generate_thumbnail_name(original_name):
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0]) return ('{0}' + XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
@staticmethod @staticmethod
def compute_location(org, course, name, revision=None, is_thumbnail=False): def compute_location(org, course, name, revision=None, is_thumbnail=False):
@@ -56,9 +57,9 @@ class StaticContent(object):
@staticmethod @staticmethod
def get_id_from_location(location): def get_id_from_location(location):
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course, return {'tag': location.tag, 'org': location.org, 'course': location.course,
'category' : location.category, 'name' : location.name, 'category': location.category, 'name': location.name,
'revision' : location.revision} 'revision': location.revision}
@staticmethod @staticmethod
def get_location_from_path(path): def get_location_from_path(path):
# remove leading / character if it is there one # remove leading / character if it is there one
@@ -117,7 +118,7 @@ class ContentStore(object):
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name) thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course, thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
thumbnail_name, is_thumbnail = True) thumbnail_name, is_thumbnail=True)
# if we're uploading an image, then let's generate a thumbnail so that we can # if we're uploading an image, then let's generate a thumbnail so that we can
# serve it up when needed without having to rescale on the fly # serve it up when needed without having to rescale on the fly
@@ -149,7 +150,3 @@ class ContentStore(object):
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e))) logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
return thumbnail_content, thumbnail_file_location return thumbnail_content, thumbnail_file_location

View File

@@ -6,6 +6,7 @@ from django.conf import settings
_CONTENTSTORE = None _CONTENTSTORE = None
def load_function(path): def load_function(path):
""" """
Load a function by name. Load a function by name.

View File

@@ -17,14 +17,14 @@ import os
class MongoContentStore(ContentStore): class MongoContentStore(ContentStore):
def __init__(self, host, db, port=27017, user=None, password=None, **kwargs): def __init__(self, host, db, port=27017, user=None, password=None, **kwargs):
logging.debug( 'Using MongoDB for static content serving at host={0} db={1}'.format(host,db)) logging.debug('Using MongoDB for static content serving at host={0} db={1}'.format(host, db))
_db = Connection(host=host, port=port, **kwargs)[db] _db = Connection(host=host, port=port, **kwargs)[db]
if user is not None and password is not None: if user is not None and password is not None:
_db.authenticate(user, password) _db.authenticate(user, password)
self.fs = gridfs.GridFS(_db) self.fs = gridfs.GridFS(_db)
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
def save(self, content): def save(self, content):
@@ -33,7 +33,7 @@ class MongoContentStore(ContentStore):
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair # Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
self.delete(id) self.delete(id)
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type, with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp: displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
fp.write(content.data) fp.write(content.data)
@@ -41,7 +41,7 @@ class MongoContentStore(ContentStore):
return content return content
def delete(self, id): def delete(self, id):
if self.fs.exists({"_id" : id}): if self.fs.exists({"_id": id}):
self.fs.delete(id) self.fs.delete(id)
def find(self, location): def find(self, location):
@@ -49,8 +49,8 @@ class MongoContentStore(ContentStore):
try: try:
with self.fs.get(id) as fp: with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
fp.uploadDate, thumbnail_location = fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None, fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path = fp.import_path if hasattr(fp, 'import_path') else None) import_path=fp.import_path if hasattr(fp, 'import_path') else None)
except NoFile: except NoFile:
raise NotFoundError() raise NotFoundError()
@@ -76,12 +76,12 @@ class MongoContentStore(ContentStore):
self.export(asset_location, output_directory) self.export(asset_location, output_directory)
def get_all_content_thumbnails_for_course(self, location): def get_all_content_thumbnails_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails = True) return self._get_all_content_for_course(location, get_thumbnails=True)
def get_all_content_for_course(self, location): def get_all_content_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails = False) return self._get_all_content_for_course(location, get_thumbnails=False)
def _get_all_content_for_course(self, location, get_thumbnails = False): def _get_all_content_for_course(self, location, get_thumbnails=False):
''' '''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example: Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
@@ -102,10 +102,7 @@ class MongoContentStore(ContentStore):
] ]
''' '''
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail", course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
course=location.course,org=location.org) course=location.course, org=location.org)
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation # 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter)) items = self.fs_files.find(location_to_query(course_filter))
return list(items) return list(items)

View File

@@ -147,37 +147,37 @@ class CourseDescriptor(SequenceDescriptor):
""" """
Return a dict which is a copy of the default grading policy Return a dict which is a copy of the default grading policy
""" """
default = {"GRADER" : [ default = {"GRADER": [
{ {
"type" : "Homework", "type": "Homework",
"min_count" : 12, "min_count": 12,
"drop_count" : 2, "drop_count": 2,
"short_label" : "HW", "short_label": "HW",
"weight" : 0.15 "weight": 0.15
}, },
{ {
"type" : "Lab", "type": "Lab",
"min_count" : 12, "min_count": 12,
"drop_count" : 2, "drop_count": 2,
"weight" : 0.15 "weight": 0.15
}, },
{ {
"type" : "Midterm Exam", "type": "Midterm Exam",
"short_label" : "Midterm", "short_label": "Midterm",
"min_count" : 1, "min_count": 1,
"drop_count" : 0, "drop_count": 0,
"weight" : 0.3 "weight": 0.3
}, },
{ {
"type" : "Final Exam", "type": "Final Exam",
"short_label" : "Final", "short_label": "Final",
"min_count" : 1, "min_count": 1,
"drop_count" : 0, "drop_count": 0,
"weight" : 0.4 "weight": 0.4
} }
], ],
"GRADE_CUTOFFS" : { "GRADE_CUTOFFS": {
"Pass" : 0.5 "Pass": 0.5
}} }}
return copy.deepcopy(default) return copy.deepcopy(default)
@@ -230,8 +230,8 @@ class CourseDescriptor(SequenceDescriptor):
# bleh, have to parse the XML here to just pull out the url_name attribute # bleh, have to parse the XML here to just pull out the url_name attribute
# I don't think it's stored anywhere in the instance. # I don't think it's stored anywhere in the instance.
course_file = StringIO(xml_data.encode('ascii','ignore')) course_file = StringIO(xml_data.encode('ascii', 'ignore'))
xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot() xml_obj = etree.parse(course_file, parser=edx_xml_parser).getroot()
policy_dir = None policy_dir = None
url_name = xml_obj.get('url_name', xml_obj.get('slug')) url_name = xml_obj.get('url_name', xml_obj.get('slug'))
@@ -329,7 +329,7 @@ class CourseDescriptor(SequenceDescriptor):
def raw_grader(self, value): def raw_grader(self, value):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf # NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
self._grading_policy['RAW_GRADER'] = value self._grading_policy['RAW_GRADER'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value self.definition['data'].setdefault('grading_policy', {})['GRADER'] = value
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
@@ -338,7 +338,7 @@ class CourseDescriptor(SequenceDescriptor):
@grade_cutoffs.setter @grade_cutoffs.setter
def grade_cutoffs(self, value): def grade_cutoffs(self, value):
self._grading_policy['GRADE_CUTOFFS'] = value self._grading_policy['GRADE_CUTOFFS'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value self.definition['data'].setdefault('grading_policy', {})['GRADE_CUTOFFS'] = value
@property @property
@@ -436,10 +436,10 @@ class CourseDescriptor(SequenceDescriptor):
scale = 300.0 # about a year scale = 300.0 # about a year
if announcement: if announcement:
days = (now - announcement).days days = (now - announcement).days
score = -exp(-days/scale) score = -exp(-days / scale)
else: else:
days = (now - start).days days = (now - start).days
score = exp(days/scale) score = exp(days / scale)
return score return score
def _sorting_dates(self): def _sorting_dates(self):
@@ -501,16 +501,16 @@ class CourseDescriptor(SequenceDescriptor):
xmoduledescriptors.append(s) xmoduledescriptors.append(s)
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) } section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)}
section_format = s.metadata.get('format', "") section_format = s.metadata.get('format', "")
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description] graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
all_descriptors.extend(xmoduledescriptors) all_descriptors.extend(xmoduledescriptors)
all_descriptors.append(s) all_descriptors.append(s)
return { 'graded_sections' : graded_sections, return {'graded_sections': graded_sections,
'all_descriptors' : all_descriptors,} 'all_descriptors': all_descriptors, }
@staticmethod @staticmethod
@@ -636,7 +636,7 @@ class CourseDescriptor(SequenceDescriptor):
# *end* of the same day, not the same time. It's going to be used as the # *end* of the same day, not the same time. It's going to be used as the
# end of the exam overall, so we don't want the exam to disappear too soon. # end of the exam overall, so we don't want the exam to disappear too soon.
# It's also used optionally as the registration end date, so time matters there too. # It's also used optionally as the registration end date, so time matters there too.
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
if self.last_eligible_appointment_date is None: if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified") raise ValueError("Last appointment date must be specified")
self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0) self.registration_start_date = self._try_parse_time('Registration_Start_Date') or time.gmtime(0)
@@ -715,4 +715,3 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def org(self): def org(self):
return self.location.org return self.location.org

View File

@@ -6,6 +6,7 @@ from xmodule.raw_module import RawDescriptor
import json import json
class DiscussionModule(XModule): class DiscussionModule(XModule):
js = {'coffee': js = {'coffee':
[resource_string(__name__, 'js/src/time.coffee'), [resource_string(__name__, 'js/src/time.coffee'),
@@ -30,6 +31,7 @@ class DiscussionModule(XModule):
self.title = xml_data.attrib['for'] self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category'] self.discussion_category = xml_data.attrib['discussion_category']
class DiscussionDescriptor(RawDescriptor): class DiscussionDescriptor(RawDescriptor):
module_class = DiscussionModule module_class = DiscussionModule
template_dir_name = "discussion" template_dir_name = "discussion"

View File

@@ -8,12 +8,14 @@ log = logging.getLogger(__name__)
ErrorLog = namedtuple('ErrorLog', 'tracker errors') ErrorLog = namedtuple('ErrorLog', 'tracker errors')
def exc_info_to_str(exc_info): def exc_info_to_str(exc_info):
"""Given some exception info, convert it into a string using """Given some exception info, convert it into a string using
the traceback.format_exception() function. the traceback.format_exception() function.
""" """
return ''.join(traceback.format_exception(*exc_info)) return ''.join(traceback.format_exception(*exc_info))
def in_exception_handler(): def in_exception_handler():
'''Is there an active exception?''' '''Is there an active exception?'''
return sys.exc_info() != (None, None, None) return sys.exc_info() != (None, None, None)
@@ -44,6 +46,7 @@ def make_error_tracker():
return ErrorLog(error_tracker, errors) return ErrorLog(error_tracker, errors)
def null_error_tracker(msg): def null_error_tracker(msg):
'''A dummy error tracker that just ignores the messages''' '''A dummy error tracker that just ignores the messages'''
pass pass

View File

@@ -49,6 +49,7 @@ def invalid_args(func, argdict):
if keywords: return set() # All accepted if keywords: return set() # All accepted
return set(argdict) - set(args) return set(argdict) - set(args)
def grader_from_conf(conf): def grader_from_conf(conf):
""" """
This creates a CourseGrader from a configuration (such as in course_settings.py). This creates a CourseGrader from a configuration (such as in course_settings.py).
@@ -210,10 +211,10 @@ class SingleSectionGrader(CourseGrader):
break break
if foundScore or generate_random_scores: if foundScore or generate_random_scores:
if generate_random_scores: # for debugging! if generate_random_scores: # for debugging!
earned = random.randint(2,15) earned = random.randint(2, 15)
possible = random.randint(earned, 15) possible = random.randint(earned, 15)
else: # We found the score else: # We found the score
earned = foundScore.earned earned = foundScore.earned
possible = foundScore.possible possible = foundScore.possible
@@ -296,8 +297,8 @@ class AssignmentFormatGrader(CourseGrader):
breakdown = [] breakdown = []
for i in range(max(self.min_count, len(scores))): for i in range(max(self.min_count, len(scores))):
if i < len(scores) or generate_random_scores: if i < len(scores) or generate_random_scores:
if generate_random_scores: # for debugging! if generate_random_scores: # for debugging!
earned = random.randint(2,15) earned = random.randint(2, 15)
possible = random.randint(earned, 15) possible = random.randint(earned, 15)
section_name = "Generated" section_name = "Generated"

View File

@@ -10,9 +10,11 @@ from lxml import etree
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class GradingServiceError(Exception): class GradingServiceError(Exception):
pass pass
class GradingService(object): class GradingService(object):
""" """
Interface to staff grading backend. Interface to staff grading backend.
@@ -35,7 +37,7 @@ class GradingService(object):
""" """
response = self.session.post(self.login_url, response = self.session.post(self.login_url,
{'username': self.username, {'username': self.username,
'password': self.password,}) 'password': self.password, })
response.raise_for_status() response.raise_for_status()

View File

@@ -1,5 +1,6 @@
from lxml import etree from lxml import etree
def check_html(html): def check_html(html):
''' '''
Check whether the passed in html string can be parsed by lxml. Check whether the passed in html string can be parsed by lxml.

View File

@@ -133,7 +133,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# TODO (ichuang): remove this after migration # TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename) # for Fall 2012 LMS migration: keep filename (and unmangled filename)
definition['filename'] = [ filepath, filename ] definition['filename'] = [filepath, filename]
return definition return definition
@@ -180,6 +180,7 @@ class AboutDescriptor(HtmlDescriptor):
""" """
template_dir_name = "about" template_dir_name = "about"
class StaticTabDescriptor(HtmlDescriptor): class StaticTabDescriptor(HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
@@ -187,6 +188,7 @@ class StaticTabDescriptor(HtmlDescriptor):
""" """
template_dir_name = "statictab" template_dir_name = "statictab"
class CourseInfoDescriptor(HtmlDescriptor): class CourseInfoDescriptor(HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located These pieces of course content are treated as HtmlModules but we need to overload where the templates are located

View File

@@ -34,7 +34,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
""" """
return {'module': self, return {'module': self,
'metadata': self.metadata, 'metadata': self.metadata,
'editable_metadata_fields' : self.editable_metadata_fields 'editable_metadata_fields': self.editable_metadata_fields
} }
def get_html(self): def get_html(self):
@@ -46,4 +46,3 @@ class MakoModuleDescriptor(XModuleDescriptor):
def editable_metadata_fields(self): def editable_metadata_fields(self):
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields] subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
return subset return subset

View File

@@ -304,7 +304,7 @@ class MongoModuleStore(ModuleStoreBase):
if location.category == 'static_tab': if location.category == 'static_tab':
course = self.get_course_for_item(item.location) course = self.get_course_for_item(item.location)
existing_tabs = course.tabs or [] existing_tabs = course.tabs or []
existing_tabs.append({'type':'static_tab', 'name' : item.metadata.get('display_name'), 'url_slug' : item.location.name}) existing_tabs.append({'type': 'static_tab', 'name': item.metadata.get('display_name'), 'url_slug': item.location.name})
course.tabs = existing_tabs course.tabs = existing_tabs
self.update_metadata(course.location, course.metadata) self.update_metadata(course.location, course.metadata)

View File

@@ -104,14 +104,14 @@ def path_to_location(modulestore, course_id, location):
# module nested in more than one positional module will work. # module nested in more than one positional module will work.
if n > 3: if n > 3:
position_list = [] position_list = []
for path_index in range(2, n-1): for path_index in range(2, n - 1):
category = path[path_index].category category = path[path_index].category
if category == 'sequential' or category == 'videosequence': if category == 'sequential' or category == 'videosequence':
section_desc = modulestore.get_instance(course_id, path[path_index]) section_desc = modulestore.get_instance(course_id, path[path_index])
child_locs = [c.location for c in section_desc.get_children()] child_locs = [c.location for c in section_desc.get_children()]
# positions are 1-indexed, and should be strings to be consistent with # positions are 1-indexed, and should be strings to be consistent with
# url parsing. # url parsing.
position_list.append(str(child_locs.index(path[path_index+1]) + 1)) position_list.append(str(child_locs.index(path[path_index + 1]) + 1))
position = "_".join(position_list) position = "_".join(position_list)
return (course_id, chapter, section, position) return (course_id, chapter, section, position)

View File

@@ -3,6 +3,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False): def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
# first check to see if the modulestore is Mongo backed # first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore): if not isinstance(modulestore, MongoModuleStore):
@@ -31,12 +32,12 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
original_loc = Location(module.location) original_loc = Location(module.location)
if original_loc.category != 'course': if original_loc.category != 'course':
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org, module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
course = dest_location.course) course=dest_location.course)
else: else:
# on the course module we also have to update the module name # on the course module we also have to update the module name
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org, module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
course = dest_location.course, name=dest_location.name) course=dest_location.course, name=dest_location.name)
print "Cloning module {0} to {1}....".format(original_loc, module.location) print "Cloning module {0} to {1}....".format(original_loc, module.location)
@@ -48,8 +49,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
new_children = [] new_children = []
for child_loc_url in module.definition['children']: for child_loc_url in module.definition['children']:
child_loc = Location(child_loc_url) child_loc = Location(child_loc_url)
child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org, child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org,
course = dest_location.course) course=dest_location.course)
new_children = new_children + [child_loc.url()] new_children = new_children + [child_loc.url()]
modulestore.update_children(module.location, new_children) modulestore.update_children(module.location, new_children)
@@ -63,8 +64,8 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
for thumb in thumbs: for thumb in thumbs:
thumb_loc = Location(thumb["_id"]) thumb_loc = Location(thumb["_id"])
content = contentstore.find(thumb_loc) content = contentstore.find(thumb_loc)
content.location = content.location._replace(org = dest_location.org, content.location = content.location._replace(org=dest_location.org,
course = dest_location.course) course=dest_location.course)
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location) print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
@@ -76,13 +77,13 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
for asset in assets: for asset in assets:
asset_loc = Location(asset["_id"]) asset_loc = Location(asset["_id"])
content = contentstore.find(asset_loc) content = contentstore.find(asset_loc)
content.location = content.location._replace(org = dest_location.org, content.location = content.location._replace(org=dest_location.org,
course = dest_location.course) course=dest_location.course)
# be sure to update the pointer to the thumbnail # be sure to update the pointer to the thumbnail
if content.thumbnail_location is not None: if content.thumbnail_location is not None:
content.thumbnail_location = content.thumbnail_location._replace(org = dest_location.org, content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
course = dest_location.course) course=dest_location.course)
print "Cloning asset {0} to {1}".format(asset_loc, content.location) print "Cloning asset {0} to {1}".format(asset_loc, content.location)
@@ -90,6 +91,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
return True return True
def delete_course(modulestore, contentstore, source_location): def delete_course(modulestore, contentstore, source_location):
# first check to see if the modulestore is Mongo backed # first check to see if the modulestore is Mongo backed
if not isinstance(modulestore, MongoModuleStore): if not isinstance(modulestore, MongoModuleStore):
@@ -119,7 +121,7 @@ def delete_course(modulestore, contentstore, source_location):
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None]) modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
for module in modules: for module in modules:
if module.category != 'course': # save deleting the course module for last if module.category != 'course': # save deleting the course module for last
print "Deleting {0}...".format(module.location) print "Deleting {0}...".format(module.location)
modulestore.delete_item(module.location) modulestore.delete_item(module.location)

Some files were not shown because too many files have changed in this diff Show More