Commit e13db29e by Brian Talbot

resolving merge from feature/btalbot/studio-softlanding

parents 70e11e82 03e7d2e7
[pep8]
ignore=E501
\ No newline at end of file
...@@ -9,6 +9,7 @@ gfortran ...@@ -9,6 +9,7 @@ gfortran
liblapack-dev liblapack-dev
libfreetype6-dev libfreetype6-dev
libpng12-dev libpng12-dev
libjpeg-dev
libxml2-dev libxml2-dev
libxslt-dev libxslt-dev
yui-compressor yui-compressor
......
...@@ -18,6 +18,8 @@ STAFF_ROLE_NAME = 'staff' ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
...@@ -8,6 +8,8 @@ import logging ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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.
......
...@@ -12,6 +12,8 @@ from logging import getLogger ...@@ -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): ...@@ -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( ...@@ -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(): ...@@ -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( ...@@ -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)
......
...@@ -2,49 +2,61 @@ from lettuce import world, step ...@@ -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')
...@@ -3,6 +3,7 @@ from student.models import User, UserProfile, Registration ...@@ -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): ...@@ -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
......
...@@ -2,54 +2,65 @@ from lettuce import world, step ...@@ -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): ...@@ -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'
......
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): ...@@ -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)
...@@ -6,11 +6,13 @@ from nose.tools import assert_true, assert_false, assert_equal ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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'
......
...@@ -2,6 +2,8 @@ from lettuce import world, step ...@@ -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): ...@@ -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):
......
...@@ -14,6 +14,7 @@ from auth.authz import _copy_course_group ...@@ -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'''
......
...@@ -15,6 +15,7 @@ from auth.authz import _delete_course_group ...@@ -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): ...@@ -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)
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.
......
import logging
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from django.http import Http404
from lxml import etree
import re
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)
else: else:
module = store.get_item(location) module = store.get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
raise Http404 # create a new one
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data'] data = module.definition['data']
if rewrite_static_links: if rewrite_static_links:
...@@ -36,9 +35,9 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link ...@@ -36,9 +35,9 @@ 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
try: try:
if location.revision is None: if location.revision is None:
module = store.get_item(location) module = store.get_item(location)
...@@ -52,7 +51,6 @@ def set_module_info(store, location, post_data): ...@@ -52,7 +51,6 @@ def set_module_info(store, location, post_data):
# presume that we have an 'Empty' template # presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location) module = store.clone_item(template_location, location)
isNew = True
if post_data.get('data') is not None: if post_data.get('data') is not None:
data = post_data['data'] data = post_data['data']
......
from factory import Factory from factory import Factory
from xmodule.modulestore import Location from datetime import datetime
from xmodule.modulestore.django import modulestore
from time import gmtime
from uuid import uuid4 from uuid import uuid4
from xmodule.timeparse import stringify_time from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed)
from django.contrib.auth.models import Group
def XMODULE_COURSE_CREATION(class_to_create, **kwargs): class UserProfileFactory(Factory):
return XModuleCourseFactory._create(class_to_create, **kwargs) FACTORY_FOR = UserProfile
def XMODULE_ITEM_CREATION(class_to_create, **kwargs): user = None
return XModuleItemFactory._create(class_to_create, **kwargs) name = 'Robot Studio'
courseware = 'course.xml'
class XModuleCourseFactory(Factory):
"""
Factory for XModule courses.
"""
ABSTRACT_FACTORY = True class RegistrationFactory(Factory):
_creation_function = (XMODULE_COURSE_CREATION,) FACTORY_FOR = Registration
@classmethod user = None
def _create(cls, target_class, *args, **kwargs): activation_key = uuid4().hex
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.get('org')
number = kwargs.get('number')
display_name = kwargs.get('display_name')
location = Location('i4x', org, number,
'course', Location.clean(display_name))
store = modulestore('direct') class UserFactory(Factory):
FACTORY_FOR = User
# Write the data to the mongo datastore username = 'robot'
new_course = store.clone_item(template, location) email = 'robot@edx.org'
password = 'test'
first_name = 'Robot'
last_name = 'Tester'
is_staff = False
is_active = True
is_superuser = False
last_login = datetime.now()
date_joined = datetime.now()
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
new_course.metadata['display_name'] = display_name
new_course.metadata['data_dir'] = uuid4().hex class GroupFactory(Factory):
new_course.metadata['start'] = stringify_time(gmtime()) FACTORY_FOR = Group
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore name = 'test_group'
store.update_metadata(new_course.location.url(), new_course.own_metadata)
return new_course
class Course: class CourseEnrollmentAllowedFactory(Factory):
pass FACTORY_FOR = CourseEnrollmentAllowed
class CourseFactory(XModuleCourseFactory): email = 'test@edx.org'
FACTORY_FOR = Course course_id = 'edX/test/2012_Fall'
template = 'i4x://edx/templates/course/Empty'
org = 'MITx'
number = '999'
display_name = 'Robot Super Course'
class XModuleItemFactory(Factory):
"""
Factory for XModule items.
"""
ABSTRACT_FACTORY = True
_creation_function = (XMODULE_ITEM_CREATION,)
@classmethod
def _create(cls, target_class, *args, **kwargs):
"""
kwargs must include parent_location, template. Can contain display_name
target_class is ignored
"""
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template'))
display_name = kwargs.get('display_name')
store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = store.clone_item(template, dest_location)
# TODO: This needs to be deleted when we have proper storage for static content
new_item.metadata['data_dir'] = parent.metadata['data_dir']
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.metadata['display_name'] = display_name
store.update_metadata(new_item.location.url(), new_item.own_metadata)
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
return new_item
class Item:
pass
class ItemFactory(XModuleItemFactory):
FACTORY_FOR = Item
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
template = 'i4x://edx/templates/chapter/Empty'
display_name = 'Section One'
\ No newline at end of file
from django.test.testcases import TestCase
from cache_toolbox.core import get_cached_content, set_cached_content, del_cached_content from cache_toolbox.core import get_cached_content, set_cached_content, del_cached_content
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from django.test import TestCase
class Content: class Content:
def __init__(self, location, content): def __init__(self, location, content):
...@@ -11,6 +12,7 @@ class Content: ...@@ -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')
...@@ -32,7 +34,3 @@ class CachingTestCase(TestCase): ...@@ -32,7 +34,3 @@ class CachingTestCase(TestCase):
'should not be stored in cache with unicodeLocation') 'should not be stored in cache with unicodeLocation')
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation), self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
'should not be stored in cache with nonUnicodeLocation') 'should not be stored in cache with nonUnicodeLocation')
from django.test.testcases import TestCase
import datetime import datetime
import time import time
from django.contrib.auth.models import User
import xmodule
from django.test.client import Client
from django.core.urlresolvers import reverse
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseSettingsEncoder
import json import json
from util import converters
import calendar import calendar
import copy
from util import converters
from util.converters import jsdate_to_time from util.converters import jsdate_to_time
from django.contrib.auth.models import User
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
import xmodule
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore from cms.djangoapps.contentstore.utils import get_modulestore
import copy
from django.test import TestCase
from utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM # YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase): 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)
...@@ -36,8 +42,15 @@ class ConvertersTestCase(TestCase): ...@@ -36,8 +42,15 @@ class ConvertersTestCase(TestCase):
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1)) self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1))
class CourseTestCase(TestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
"""
These tests need a user in the DB so that the django Test Client
can log them in.
They inherit from the ModuleStoreTestCase class so that the mongodb collection
will be cleared out before each test case execution and deleted
afterwards.
"""
uname = 'testuser' uname = 'testuser'
email = 'test+courses@edx.org' email = 'test+courses@edx.org'
password = 'foo' password = 'foo'
...@@ -52,36 +65,16 @@ class CourseTestCase(TestCase): ...@@ -52,36 +65,16 @@ class CourseTestCase(TestCase):
self.user.is_staff = True self.user.is_staff = True
self.user.save() self.user.save()
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
xmodule.templates.update_templates()
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
self.course_data = { t = 'i4x://edx/templates/course/Empty'
'template': 'i4x://edx/templates/course/Empty', o = 'MITx'
'org': 'MITx', n = '999'
'number': '999', dn = 'Robot Super Course'
'display_name': '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)
self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course')
self.create_course()
def tearDown(self):
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
def create_course(self):
"""Create new course"""
self.client.post(reverse('create_new_course'), self.course_data)
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self): def test_virgin_fetch(self):
...@@ -126,6 +119,7 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -126,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)
...@@ -146,27 +140,22 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -146,27 +140,22 @@ class CourseDetailsViewTest(CourseTestCase):
else: else:
return None return None
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,
'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)
# 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")
...@@ -199,6 +188,7 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -199,6 +188,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)
...@@ -271,5 +261,3 @@ class CourseGradingTest(CourseTestCase): ...@@ -271,5 +261,3 @@ class CourseGradingTest(CourseTestCase):
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1 test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2") self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
...@@ -2,27 +2,28 @@ from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCas ...@@ -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")
......
from django.test.testcases import TestCase
from cms.djangoapps.contentstore import utils from cms.djangoapps.contentstore import utils
import mock import mock
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")
......
import json
import copy
from time import time
from django.test import TestCase
from django.conf import settings
from student.models import Registration
from django.contrib.auth.models import User
import xmodule.modulestore.django
from xmodule.templates import update_templates
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
def _pre_setup(self):
super(ModuleStoreTestCase, self)._pre_setup()
# Use the current seconds since epoch to differentiate
# the mongo collections on jenkins.
sec_since_epoch = '%s' % int(time() * 100)
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
settings.MODULESTORE = self.test_MODULESTORE
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {}
update_templates()
def _post_teardown(self):
# Make sure you flush out the modulestore.
# Drop the collection at the end of the test,
# otherwise there will be lingering collections leftover
# from executing the tests.
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
settings.MODULESTORE = self.orig_MODULESTORE
super(ModuleStoreTestCase, self)._post_teardown()
def parse_json(response):
"""Parse response, which is assumed to be json"""
return json.loads(response.content)
def user(email):
"""look up a user by email"""
return User.objects.get(email=email)
def registration(email):
"""look up registration object by email"""
return Registration.objects.get(user__email=email)
...@@ -5,6 +5,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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.
......
...@@ -47,12 +47,12 @@ class CourseGradingModel(object): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -33,7 +33,7 @@ MITX_FEATURES = { ...@@ -33,7 +33,7 @@ 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
...@@ -165,13 +165,6 @@ STATICFILES_DIRS = [ ...@@ -165,13 +165,6 @@ STATICFILES_DIRS = [
# This is how you would use the textbook images locally # This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images") # ("book", ENV_ROOT / "book_images")
] ]
if os.path.isdir(GITHUB_REPO_ROOT):
STATICFILES_DIRS += [
# TODO (cpennington): When courses aren't loaded from github, remove this
(course_dir, GITHUB_REPO_ROOT / course_dir)
for course_dir in os.listdir(GITHUB_REPO_ROOT)
if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
]
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
...@@ -229,7 +222,7 @@ PIPELINE_JS = { ...@@ -229,7 +222,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': {
......
...@@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG ...@@ -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 = { ...@@ -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',
} }
} }
......
...@@ -9,8 +9,6 @@ import socket ...@@ -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
...@@ -11,7 +11,6 @@ from .common import * ...@@ -11,7 +11,6 @@ from .common import *
import os import os
from path import path from path import path
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--with-xunit'] NOSE_ARGS = ['--with-xunit']
...@@ -63,7 +62,7 @@ CONTENTSTORE = { ...@@ -63,7 +62,7 @@ CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': { 'OPTIONS': {
'host': 'localhost', 'host': 'localhost',
'db' : 'xcontent', 'db': 'xcontent',
} }
} }
...@@ -72,17 +71,6 @@ DATABASES = { ...@@ -72,17 +71,6 @@ DATABASES = {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db", 'NAME': ENV_ROOT / "db" / "cms.db",
}, },
# The following are for testing purposes...
'edX/toy/2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
}
} }
LMS_BASE = "localhost:8000" LMS_BASE = "localhost:8000"
......
...@@ -6,26 +6,26 @@ ...@@ -6,26 +6,26 @@
</div> </div>
<div class="field text" id="field-course-grading-assignment-shortname"> <div class="field text" id="field-course-grading-assignment-shortname">
<label for="course-grading-shortname">Abbreviation:</label> <label for="course-grading-assignment-shortname">Abbreviation:</label>
<input type="text" class="short" id="course-grading-assignment-shortname" value="<%= model.get('short_label') %>" /> <input type="text" class="short" id="course-grading-assignment-shortname" value="<%= model.get('short_label') %>" />
<span class="tip tip-inline">e.g. HW, Midterm</span> <span class="tip tip-inline">e.g. HW, Midterm</span>
</div> </div>
<div class="field text" id="field-course-grading-assignment-gradeweight"> <div class="field text" id="field-course-grading-assignment-gradeweight">
<label for="course-grading-gradeweight">Weight of Total Grade</label> <label for="course-grading-assignment-gradeweight">Weight of Total Grade</label>
<input type="text" class="short" id="course-grading-assignment-gradeweight" value = "<%= model.get('weight') %>" /> <input type="text" class="short" id="course-grading-assignment-gradeweight" value = "<%= model.get('weight') %>" />
<span class="tip tip-inline">e.g. 25%</span> <span class="tip tip-inline">e.g. 25%</span>
</div> </div>
<div class="field text" id="field-course-grading-assignment-totalassignments"> <div class="field text" id="field-course-grading-assignment-totalassignments">
<label for="course-grading-gradeweight">Total <label for="course-grading-assignment-totalassignments">Total
Number</label> Number</label>
<input type="text" class="short" id="course-grading-assignment-totalassignments" value = "<%= model.get('min_count') %>" /> <input type="text" class="short" id="course-grading-assignment-totalassignments" value = "<%= model.get('min_count') %>" />
<span class="tip tip-inline">total exercises assigned</span> <span class="tip tip-inline">total exercises assigned</span>
</div> </div>
<div class="field text" id="field-course-grading-assignment-droppable"> <div class="field text" id="field-course-grading-assignment-droppable">
<label for="course-grading-gradeweight">Number of <label for="course-grading-assignment-droppable">Number of
Droppable</label> Droppable</label>
<input type="text" class="short" id="course-grading-assignment-droppable" value = "<%= model.get('drop_count') %>" /> <input type="text" class="short" id="course-grading-assignment-droppable" value = "<%= model.get('drop_count') %>" />
<span class="tip tip-inline">total exercises that won't be graded</span> <span class="tip tip-inline">total exercises that won't be graded</span>
......
...@@ -39,15 +39,20 @@ $(document).ready(function() { ...@@ -39,15 +39,20 @@ $(document).ready(function() {
$('.unit .item-actions .delete-button').bind('click', deleteUnit); $('.unit .item-actions .delete-button').bind('click', deleteUnit);
$('.new-unit-item').bind('click', createNewUnit); $('.new-unit-item').bind('click', createNewUnit);
// nav-related
$('body').addClass('js'); $('body').addClass('js');
// nav - dropdown related
$body.click(function(e){
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
$('.nav-dropdown .nav-item .title').removeClass('is-selected');
});
$('.nav-dropdown .nav-item .title').click(function(e){ $('.nav-dropdown .nav-item .title').click(function(e){
$subnav = $(this).parent().find('.wrapper-nav-sub'); $subnav = $(this).parent().find('.wrapper-nav-sub');
$title = $(this).parent().find('.title'); $title = $(this).parent().find('.title');
e.preventDefault(); e.preventDefault();
e.stopPropagation();
if ($subnav.hasClass('is-shown')) { if ($subnav.hasClass('is-shown')) {
$subnav.removeClass('is-shown'); $subnav.removeClass('is-shown');
......
...@@ -61,7 +61,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -61,7 +61,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
url: function() { url: function() {
var location = this.get('location'); var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details'; return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/details';
}, },
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g, _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
......
...@@ -28,7 +28,7 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({ ...@@ -28,7 +28,7 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
}, },
url : function() { url : function() {
var location = this.get('course_location'); var location = this.get('course_location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading'; return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/grading';
}, },
gracePeriodToDate : function() { gracePeriodToDate : function() {
var newDate = new Date(); var newDate = new Date();
...@@ -120,7 +120,7 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({ ...@@ -120,7 +120,7 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
model : CMS.Models.Settings.CourseGrader, model : CMS.Models.Settings.CourseGrader,
course_location : null, // must be set to a Location object course_location : null, // must be set to a Location object
url : function() { url : function() {
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name') + '/'; return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/';
}, },
sumWeights : function() { sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0); return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
......
...@@ -59,6 +59,9 @@ $(document).ready(function() { ...@@ -59,6 +59,9 @@ $(document).ready(function() {
greedy: true greedy: true
}); });
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
}); });
CMS.HesitateEvent.toggleXpandHesitation = null; CMS.HesitateEvent.toggleXpandHesitation = null;
...@@ -202,13 +205,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) { ...@@ -202,13 +205,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
children = _.without(children, ui.draggable.data('id')); children = _.without(children, ui.draggable.data('id'));
} }
// add to this parent (figure out where) // add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) { for (var i = 0, bump = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) { if (ui.draggable.is(_els[i])) {
bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c
// it's not in that list
}
else if (ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els // insert at i in children and _els
ui.draggable.insertBefore($(_els[i])); ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below) // TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;"); ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id')); children.splice(i + bump, 0, ui.draggable.data('id'));
break; break;
} }
} }
......
...@@ -96,7 +96,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -96,7 +96,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
time = 0; time = 0;
} }
var newVal = new Date(date.getTime() + time * 1000); var newVal = new Date(date.getTime() + time * 1000);
if (cacheModel.get(fieldName).getTime() !== newVal.getTime()) { if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
cacheModel.save(fieldName, newVal); cacheModel.save(fieldName, newVal);
} }
} }
...@@ -190,3 +190,4 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -190,3 +190,4 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
} }
}); });
...@@ -145,6 +145,13 @@ body.signup, body.signin { ...@@ -145,6 +145,13 @@ body.signup, body.signin {
:-ms-input-placeholder { :-ms-input-placeholder {
color: $gray-l3; color: $gray-l3;
} }
&:focus {
+ .tip {
color: $gray;
}
}
} }
textarea.long { textarea.long {
...@@ -163,6 +170,7 @@ body.signup, body.signin { ...@@ -163,6 +170,7 @@ body.signup, body.signin {
} }
.tip { .tip {
@include transition(color, 0.15s, ease-in-out);
@include font-size(13); @include font-size(13);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
// basic reset // basic reset
html { html {
font-size: 62.5%; font-size: 62.5%;
overflow-y: scroll;
} }
body { body {
...@@ -194,6 +195,22 @@ h1 { ...@@ -194,6 +195,22 @@ h1 {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.nav-related {
.nav-item {
margin-bottom: ($baseline/4);
border-bottom: 1px dotted $gray-l4;
padding-bottom: ($baseline/4);
&:last-child {
margin-bottom: 0;
border: none;
padding-bottom: 0;
}
}
}
} }
} }
...@@ -606,3 +623,13 @@ body.hide-wip { ...@@ -606,3 +623,13 @@ body.hide-wip {
display: none; display: none;
} }
} }
// ====================
// needed fudges for now
body.dashboard {
.my-classes {
margin-top: $baseline;
}
}
\ No newline at end of file
...@@ -53,9 +53,6 @@ ...@@ -53,9 +53,6 @@
float: right; float: right;
} }
// general nav styles
// ====================
// ==================== // ====================
// specific elements - branding // specific elements - branding
...@@ -66,7 +63,7 @@ ...@@ -66,7 +63,7 @@
.branding { .branding {
position: relative; position: relative;
margin: 0 ($baseline*0.75) 0 0; margin: 0 ($baseline/2) 0 0;
padding-right: ($baseline*0.75); padding-right: ($baseline*0.75);
a { a {
...@@ -109,18 +106,21 @@ ...@@ -109,18 +106,21 @@
width: 1px; width: 1px;
} }
.course-number, .course-title { .course-org {
display: block; margin-right: ($baseline/4);
} }
.course-number { .course-number, .course-org {
@include font-size(12); @include font-size(12);
display: inline-block;
} }
.course-title { .course-title {
display: block;
width: 100%; width: 100%;
max-width: 220px; max-width: 220px;
overflow: hidden; overflow: hidden;
margin-top: -4px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@include font-size(16); @include font-size(16);
...@@ -151,23 +151,19 @@ ...@@ -151,23 +151,19 @@
font-weight: 600; font-weight: 600;
color: $gray-d3; color: $gray-d3;
&:hover, &:active { &:hover, &:active, &.is-selected {
color: $blue;
.icon-expand {
color: $blue; color: $blue;
} }
}
.label-prefix { .label-prefix {
display: block; display: block;
@include font-size(11); @include font-size(11);
font-weight: 400; font-weight: 400;
} }
&.is-selected {
color: $blue-d1;
.icon-expand {
color: $blue-d1;
}
}
} }
// specific nav items // specific nav items
...@@ -203,11 +199,17 @@ ...@@ -203,11 +199,17 @@
.account-username { .account-username {
display: inline-block; display: inline-block;
vertical-align: middle;
width: 80%; width: 80%;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.icon-expand {
display: inline-block;
vertical-align: middle;
}
} }
} }
...@@ -222,6 +224,7 @@ ...@@ -222,6 +224,7 @@
.icon-expand { .icon-expand {
@include font-size(14); @include font-size(14);
@include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out); @include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out);
display: inline-block;
margin-left: 2px; margin-left: 2px;
opacity: 0.5; opacity: 0.5;
color: $gray-l2; color: $gray-l2;
...@@ -306,7 +309,7 @@ ...@@ -306,7 +309,7 @@
.wrapper-nav-sub { .wrapper-nav-sub {
top: 27px; top: 27px;
left: auto; left: auto;
right: -($baseline/2); right: -13px;
width: 110px; width: 110px;
} }
...@@ -513,6 +516,7 @@ body.course.export .nav-course-tools-export, ...@@ -513,6 +516,7 @@ body.course.export .nav-course-tools-export,
a { a {
color: $blue; color: $blue;
pointer-events: none;
} }
} }
...@@ -540,6 +544,7 @@ body.signin .nav-not-signedin-signup { ...@@ -540,6 +544,7 @@ body.signin .nav-not-signedin-signup {
.nav-dropdown { .nav-dropdown {
.nav-item .title { .nav-item .title {
outline: 0;
cursor: pointer; cursor: pointer;
} }
} }
......
...@@ -36,7 +36,9 @@ ...@@ -36,7 +36,9 @@
color: $gray-d2; color: $gray-d2;
header { header {
border: none;
padding-bottom: 0;
margin-bottom: 0;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
...@@ -238,7 +240,6 @@ ...@@ -238,7 +240,6 @@
margin-right: flex-gutter(); margin-right: flex-gutter();
padding: ($baseline*0.75) $baseline; padding: ($baseline*0.75) $baseline;
color: $gray-l1; color: $gray-l1;
cursor: pointer;
.title { .title {
@include font-size(16); @include font-size(16);
...@@ -252,7 +253,7 @@ ...@@ -252,7 +253,7 @@
background: $blue-l5; background: $blue-l5;
top: -($baseline/5); top: -($baseline/5);
.title, strong { .title {
color: $blue; color: $blue;
} }
} }
......
...@@ -47,6 +47,7 @@ body.course.settings { ...@@ -47,6 +47,7 @@ body.course.settings {
// UI hints/tips/messages // UI hints/tips/messages
.tip { .tip {
@include transition(color, 0.15s, ease-in-out);
@include font-size(13); @include font-size(13);
display: block; display: block;
margin-top: ($baseline/4); margin-top: ($baseline/4);
...@@ -127,6 +128,13 @@ body.course.settings { ...@@ -127,6 +128,13 @@ body.course.settings {
&.error { &.error {
border-color: $red; border-color: $red;
} }
&:focus {
+ .tip {
color: $gray;
}
}
} }
textarea.long { textarea.long {
...@@ -157,10 +165,10 @@ body.course.settings { ...@@ -157,10 +165,10 @@ body.course.settings {
@include box-sizing(border-box); @include box-sizing(border-box);
@include border-radius(3px); @include border-radius(3px);
background: $gray-l5; background: $gray-l5;
padding: ($baseline/2); padding: $baseline;
&:last-child { &:last-child {
padding-bottom: ($baseline/2); padding-bottom: $baseline;
} }
.actions { .actions {
......
...@@ -137,6 +137,8 @@ ...@@ -137,6 +137,8 @@
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
......
...@@ -39,14 +39,18 @@ ...@@ -39,14 +39,18 @@
<h1 class="title-1">My Courses</h1> <h1 class="title-1">My Courses</h1>
</div> </div>
% if user.is_active:
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
% if not disable_course_creation:
<a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a> <a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a>
% endif
</li> </li>
</ul> </ul>
</nav> </nav>
% endif
</header> </header>
</div> </div>
......
...@@ -8,15 +8,15 @@ ...@@ -8,15 +8,15 @@
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<header> <header>
<h1 class="title title-1">Sign into edX Studio</h1> <h1 class="title title-1">Sign In to edX Studio</h1>
<a href="" class="action action-signin">Don't have a Studio Account? Sign up!</a> <a href="${reverse('signup')}" class="action action-signin">Don't have a Studio Account? Sign up!</a>
</header> </header>
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post"> <form id="login_form" method="post" action="login_post">
<fieldset> <fieldset>
<legend class="sr">Required Information to Sign into edX Studio</legend> <legend class="sr">Required Information to Sign In to edX Studio</legend>
<ol class="list-input"> <ol class="list-input">
<li class="field text required" id="field-email"> <li class="field text required" id="field-email">
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
</fieldset> </fieldset>
<div class="form-actions"> <div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Sign into edX Studio</button> <button type="submit" id="submit" name="submit" class="action action-primary">Sign In to edX Studio</button>
</div> </div>
<!-- no honor code for CMS, but need it because we're using the lms student object --> <!-- no honor code for CMS, but need it because we're using the lms student object -->
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<div class="bit"> <div class="bit">
<h3 class="title-3">Need Help?</h3> <h3 class="title-3">Need Help?</h3>
<p>Having trouble with your account? Use <a href="" rel="external">our support center</a> to look over self help steps, find solutions others have found to the same problem, or let us know of your issue.</p> <p>Having trouble with your account? Use <a href="http://help.edge.edx.org" rel="external">our support center</a> to look over self help steps, find solutions others have found to the same problem, or let us know of your issue.</p>
</div> </div>
</aside> </aside>
</section> </section>
...@@ -77,7 +77,11 @@ ...@@ -77,7 +77,11 @@
submit_data, submit_data,
function(json) { function(json) {
if(json.success) { if(json.success) {
location.href = "${reverse('index')}"; var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1) {
location.href = next[1];
}
else location.href = "${reverse('homepage')}";
} else if($('#login_error').length == 0) { } else if($('#login_error').length == 0) {
$('#login_form').prepend('<div id="login_error" class="message message-status error">' + json.value + '</span></div>'); $('#login_form').prepend('<div id="login_error" class="message message-status error">' + json.value + '</span></div>');
$('#login_error').addClass('is-shown'); $('#login_error').addClass('is-shown');
......
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
$cancelButton.bind('click', hideNewUserForm); $cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm); $('.new-user-button').bind('click', showNewUserForm);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); $('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() { $('.remove-user').click(function() {
$.ajax({ $.ajax({
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
from contentstore import utils from contentstore import utils
%> %>
<%block name="jsextra"> <%block name="jsextra">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script> <script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
...@@ -81,7 +82,7 @@ from contentstore import utils ...@@ -81,7 +82,7 @@ from contentstore import utils
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" /> <input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" />
</li> </li>
</ol> </ol>
<span class="tip tip-stacked">These are used in <a href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span> <span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
</section> </section>
<hr class="divide" /> <hr class="divide" />
...@@ -158,7 +159,7 @@ from contentstore import utils ...@@ -158,7 +159,7 @@ from contentstore import utils
<section class="group-settings marketing"> <section class="group-settings marketing">
<header> <header>
<h2 class="title-2">Introducing Your Course</h2> <h2 class="title-2">Introducing Your Course</h2>
<span class="tip">Information for perspective students</span> <span class="tip">Information for prospective students</span>
</header> </header>
<ol class="list-input"> <ol class="list-input">
...@@ -223,6 +224,7 @@ from contentstore import utils ...@@ -223,6 +224,7 @@ from contentstore import utils
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
...@@ -145,6 +145,7 @@ from contentstore import utils ...@@ -145,6 +145,7 @@ from contentstore import utils
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
</ul> </ul>
</nav> </nav>
% endif % endif
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<section class="content"> <section class="content">
<header> <header>
<h1 class="title title-1">Sign Up for edX Studio</h1> <h1 class="title title-1">Sign Up for edX Studio</h1>
<a href="" class="action action-signin">Already have a Studio Account? Sign in</a> <a href="${reverse('login')}" class="action action-signin">Already have a Studio Account? Sign in</a>
</header> </header>
<p class="introduction">Ready to start creating online courses? Sign up below and start creating your first edX course today.</p> <p class="introduction">Ready to start creating online courses? Sign up below and start creating your first edX course today.</p>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a> <a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a>
</li> </li>
<li class="nav-item nav-peripheral-contact"> <li class="nav-item nav-peripheral-contact">
<a href="#">Contact edX</a> <a href="https://www.edx.org/contact" rel="external">Contact edX</a>
</li> </li>
% if user.is_authenticated(): % if user.is_authenticated():
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="info-course"> <div class="info-course">
<h2 class="sr">Current Course:</h2> <h2 class="sr">Current Course:</h2>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}"> <a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
<span class="course-number">PH207x:</span> <span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
<span class="course-title" title="${context_course.display_name}">${context_course.display_name}</span> <span class="course-title" title="${context_course.display_name}">${context_course.display_name}</span>
</a> </a>
</div> </div>
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
<div class="wrapper wrapper-nav-sub"> <div class="wrapper wrapper-nav-sub">
<div class="nav-sub"> <div class="nav-sub">
<ul> <ul>
<li class="nav-item nav-account-dashboard"><a href="#">My Courses</a></li> <li class="nav-item nav-account-dashboard"><a href="/">My Courses</a></li>
<li class="nav-item nav-account-help"><a href="http://help.edge.edx.org/" rel="external">Studio Help</a></li> <li class="nav-item nav-account-help"><a href="http://help.edge.edx.org/" rel="external">Studio Help</a></li>
<li class="nav-item nav-account-signout"><a class="action action-signout" href="${reverse('logout')}">Sign Out</a></li> <li class="nav-item nav-account-signout"><a class="action action-signout" href="${reverse('logout')}">Sign Out</a></li>
</ul> </ul>
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
<h2 class="sr">You're not currently signed in</h2> <h2 class="sr">You're not currently signed in</h2>
<ol> <ol>
<li class="nav-item nav-not-signedin-hiw"> <li class="nav-item nav-not-signedin-hiw">
<a href="#">How Studio Works</a> <a href="/">How Studio Works</a>
</li> </li>
<li class="nav-item nav-not-signedin-help"> <li class="nav-item nav-not-signedin-help">
<a href="http://help.edge.edx.org/" rel="external">Studio Help</a> <a href="http://help.edge.edx.org/" rel="external">Studio Help</a>
......
...@@ -6,7 +6,8 @@ from django.conf.urls import patterns, include, url ...@@ -6,7 +6,8 @@ from django.conf.urls import patterns, include, url
# admin.autodiscover() # admin.autodiscover()
urlpatterns = ('', urlpatterns = ('',
url(r'^$', 'contentstore.views.index', name='index'), url(r'^$', 'contentstore.views.howitworks', name='homepage'),
url(r'^listing', 'contentstore.views.index', name='index'),
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^subsection/(?P<location>.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'),
url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'), url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
......
...@@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS ...@@ -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): ...@@ -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))
...@@ -12,7 +12,7 @@ from xmodule.exceptions import NotFoundError ...@@ -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)
......
...@@ -13,6 +13,7 @@ from .models import CourseUserGroup ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -211,4 +217,3 @@ def delete_empty_cohort(course_id, name):
name, course_id)) name, course_id))
cohort.delete() cohort.delete()
...@@ -5,6 +5,7 @@ from django.db import models ...@@ -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): ...@@ -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)
...@@ -2,7 +2,7 @@ import django.test ...@@ -2,7 +2,7 @@ import django.test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from override_settings import override_settings from django.test.utils import override_settings
from course_groups.models import CourseUserGroup from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts, from course_groups.cohorts import (get_cohort, get_course_cohorts,
...@@ -14,6 +14,7 @@ from xmodule.modulestore.django import modulestore, _MODULESTORES ...@@ -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): ...@@ -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): ...@@ -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.")
...@@ -22,6 +22,7 @@ import track.views ...@@ -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): ...@@ -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): ...@@ -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):
......
...@@ -5,8 +5,9 @@ django admin pages for courseware model ...@@ -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)
...@@ -12,6 +12,7 @@ file and check it in at the same time as your model changes. To do that, ...@@ -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): ...@@ -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
...@@ -13,6 +13,7 @@ from django.test import TestCase, LiveServerTestCase ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -12,6 +12,7 @@ import mitxmako.middleware ...@@ -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): ...@@ -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
......
...@@ -20,6 +20,8 @@ from mitxmako import middleware ...@@ -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
......
...@@ -2,6 +2,7 @@ from django.template import loader ...@@ -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): ...@@ -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 ...@@ -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)
...@@ -13,14 +13,21 @@ log = logging.getLogger(__name__) ...@@ -13,14 +13,21 @@ log = logging.getLogger(__name__)
def _url_replace_regex(prefix): def _url_replace_regex(prefix):
"""
Match static urls in quotes that don't end in '?raw'.
To anyone contemplating making this more complicated:
http://xkcd.com/1171/
"""
return r""" return r"""
(?x) # flags=re.VERBOSE (?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes (?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # theeprefix (?P<prefix>{prefix}) # the prefix
(?P<rest>.*?) # everything else in the url (?P<rest>.*?) # everything else in the url
(?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
...@@ -73,21 +80,30 @@ def replace_static_urls(text, data_directory, course_namespace=None): ...@@ -73,21 +80,30 @@ def replace_static_urls(text, data_directory, course_namespace=None):
quote = match.group('quote') quote = match.group('quote')
rest = match.group('rest') rest = match.group('rest')
# Don't mess with things that end in '?raw'
if rest.endswith('?raw'):
return original
# course_namespace is not None, then use studio style urls # course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace) url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# If we're in debug mode, and the file as requested exists, then don't change the links # In debug mode, if we can find the url as is,
elif (settings.DEBUG and finders.find(rest, True)): elif settings.DEBUG and finders.find(rest, True):
return original return original
# Otherwise, look the file up in staticfiles_storage without the data directory # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else: else:
course_path = "/".join((data_directory, rest))
try: try:
if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest) url = staticfiles_storage.url(rest)
else:
url = staticfiles_storage.url(course_path)
# And if that fails, assume that it's course content, and add manually data directory # And if that fails, assume that it's course content, and add manually data directory
except Exception as err: except Exception as err:
log.warning("staticfiles_storage couldn't find path {0}: {1}".format( log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
rest, str(err))) rest, str(err)))
url = "".join([prefix, data_directory, '/', rest]) url = "".join([prefix, course_path])
return "".join([quote, url, quote]) return "".join([quote, url, quote])
......
from nose.tools import assert_equals import re
from static_replace import replace_static_urls, replace_course_urls
from nose.tools import assert_equals, assert_true, assert_false
from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex)
from mock import patch, Mock from mock import patch, Mock
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
...@@ -24,15 +27,24 @@ def test_multi_replace(): ...@@ -24,15 +27,24 @@ def test_multi_replace():
) )
@patch('static_replace.finders') @patch('static_replace.staticfiles_storage')
@patch('static_replace.settings') def test_storage_url_exists(mock_storage):
def test_debug_no_modify(mock_settings, mock_finders): mock_storage.exists.return_value = True
mock_settings.DEBUG = True mock_storage.url.return_value = '/static/file.png'
mock_finders.find.return_value = True
assert_equals('"/static/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
mock_storage.exists.called_once_with('file.png')
mock_storage.url.called_once_with('data_dir/file.png')
assert_equals(STATIC_SOURCE, replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY)) @patch('static_replace.staticfiles_storage')
def test_storage_url_not_exists(mock_storage):
mock_storage.exists.return_value = False
mock_storage.url.return_value = '/static/data_dir/file.png'
mock_finders.find.assert_called_once_with('file.png', True) assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
mock_storage.exists.called_once_with('file.png')
mock_storage.url.called_once_with('file.png')
@patch('static_replace.StaticContent') @patch('static_replace.StaticContent')
...@@ -53,12 +65,47 @@ def test_mongo_filestore(mock_modulestore, mock_static_content): ...@@ -53,12 +65,47 @@ 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')
def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
mock_modulestore.return_value = Mock(XMLModuleStore) mock_modulestore.return_value = Mock(XMLModuleStore)
mock_settings.DEBUG = False
mock_storage.url.side_effect = Exception mock_storage.url.side_effect = Exception
mock_storage.exists.return_value = True
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
mock_storage.exists.return_value = False
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY)) assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
def test_raw_static_check():
"""
Make sure replace_static_urls leaves alone things that end in '.raw'
"""
path = '"/static/foo.png?raw"'
assert_equals(path, replace_static_urls(path, DATA_DIRECTORY))
text = 'text <tag a="/static/js/capa/protex/protex.nocache.js?raw"/><div class="'
assert_equals(path, replace_static_urls(path, text))
def test_regex():
yes = ('"/static/foo.png"',
'"/static/foo.png"',
"'/static/foo.png'")
no = ('"/not-static/foo.png"',
'"/static/foo', # no matching quote
)
regex = _url_replace_regex('/static/')
for s in yes:
print 'Should match: {0!r}'.format(s)
assert_true(re.match(regex, s))
for s in no:
print 'Should not match: {0!r}'.format(s)
assert_false(re.match(regex, s))
...@@ -10,6 +10,7 @@ import sys ...@@ -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,
......
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
import os import os
from override_settings import override_settings from django.test.utils import override_settings
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from status import get_site_status_msg from status import get_site_status_msg
......
...@@ -57,7 +57,7 @@ from student.userprofile. ''' ...@@ -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()
...@@ -3,6 +3,7 @@ from optparse import make_option ...@@ -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',
......
...@@ -8,6 +8,7 @@ from student.models import UserProfile, CourseEnrollment ...@@ -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): ...@@ -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.
......
...@@ -49,19 +49,19 @@ class Command(BaseCommand): ...@@ -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): ...@@ -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)
...@@ -116,4 +116,3 @@ class Command(BaseCommand): ...@@ -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)
...@@ -9,6 +9,7 @@ from student.views import course_from_id ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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."
...@@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -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): ...@@ -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): ...@@ -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."
...@@ -49,7 +49,7 @@ class Command(BaseCommand): ...@@ -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): ...@@ -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): ...@@ -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))
......
...@@ -107,6 +107,7 @@ class UserProfile(models.Model): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 = ( ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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))
...@@ -17,6 +17,7 @@ COURSE_2 = 'edx/full/6.002_Spring_2012' ...@@ -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): ...@@ -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),
......
...@@ -2,21 +2,20 @@ from django.db import models ...@@ -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
...@@ -17,19 +17,21 @@ from track.models import TrackingLog ...@@ -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
...@@ -91,9 +93,10 @@ def server_track(request, event_type, event, page=None): ...@@ -91,9 +93,10 @@ def server_track(request, event_type, event, page=None):
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=''): ...@@ -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})
...@@ -58,4 +58,3 @@ def cache_if_anonymous(view_func): ...@@ -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
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,16 +11,20 @@ def time_to_date(time_obj): ...@@ -9,16 +11,20 @@ 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
""" """
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):
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable # ISO format but ignores time zone assuming it's Z.
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, long, float)):
return time.gmtime(field / 1000) return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time): elif isinstance(field, time.struct_time):
return field return field
else:
raise ValueError("Couldn't convert %r to time" % field)
...@@ -13,7 +13,7 @@ def expect_json(view_function): ...@@ -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))
......
...@@ -93,6 +93,7 @@ def accepts(request, media_type): ...@@ -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"""
......
...@@ -12,6 +12,7 @@ from xmodule.vertical_module import VerticalModule ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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.
...@@ -114,35 +117,35 @@ def add_histogram(get_html, module, user): ...@@ -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): ...@@ -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
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment