Merge pull request #1429 from MITx/feature/cale/pep8-autofix
Feature/cale/pep8 autofix
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ STAFF_ROLE_NAME = 'staff'
|
|||||||
# we're just making a Django group for each location/role combo
|
# we're just making a Django group for each location/role combo
|
||||||
# to do this we're just creating a Group name which is a formatted string
|
# to do this we're just creating a Group name which is a formatted string
|
||||||
# of those two variables
|
# of those two variables
|
||||||
|
|
||||||
|
|
||||||
def get_course_groupname_for_role(location, role):
|
def get_course_groupname_for_role(location, role):
|
||||||
loc = Location(location)
|
loc = Location(location)
|
||||||
# hack: check for existence of a group name in the legacy LMS format <role>_<course>
|
# hack: check for existence of a group name in the legacy LMS format <role>_<course>
|
||||||
@@ -25,11 +27,12 @@ def get_course_groupname_for_role(location, role):
|
|||||||
# more information
|
# more information
|
||||||
groupname = '{0}_{1}'.format(role, loc.course)
|
groupname = '{0}_{1}'.format(role, loc.course)
|
||||||
|
|
||||||
if len(Group.objects.filter(name = groupname)) == 0:
|
if len(Group.objects.filter(name=groupname)) == 0:
|
||||||
groupname = '{0}_{1}'.format(role,loc.course_id)
|
groupname = '{0}_{1}'.format(role, loc.course_id)
|
||||||
|
|
||||||
return groupname
|
return groupname
|
||||||
|
|
||||||
|
|
||||||
def get_users_in_course_group_by_role(location, role):
|
def get_users_in_course_group_by_role(location, role):
|
||||||
groupname = get_course_groupname_for_role(location, role)
|
groupname = get_course_groupname_for_role(location, role)
|
||||||
(group, created) = Group.objects.get_or_create(name=groupname)
|
(group, created) = Group.objects.get_or_create(name=groupname)
|
||||||
@@ -39,6 +42,8 @@ def get_users_in_course_group_by_role(location, role):
|
|||||||
'''
|
'''
|
||||||
Create all permission groups for a new course and subscribe the caller into those roles
|
Create all permission groups for a new course and subscribe the caller into those roles
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def create_all_course_groups(creator, location):
|
def create_all_course_groups(creator, location):
|
||||||
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
|
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
|
||||||
create_new_course_group(creator, location, STAFF_ROLE_NAME)
|
create_new_course_group(creator, location, STAFF_ROLE_NAME)
|
||||||
@@ -46,7 +51,7 @@ def create_all_course_groups(creator, location):
|
|||||||
|
|
||||||
def create_new_course_group(creator, location, role):
|
def create_new_course_group(creator, location, role):
|
||||||
groupname = get_course_groupname_for_role(location, role)
|
groupname = get_course_groupname_for_role(location, role)
|
||||||
(group, created) =Group.objects.get_or_create(name=groupname)
|
(group, created) = Group.objects.get_or_create(name=groupname)
|
||||||
if created:
|
if created:
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
@@ -59,6 +64,8 @@ def create_new_course_group(creator, location, role):
|
|||||||
This is to be called only by either a command line code path or through a app which has already
|
This is to be called only by either a command line code path or through a app which has already
|
||||||
asserted permissions
|
asserted permissions
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def _delete_course_group(location):
|
def _delete_course_group(location):
|
||||||
# remove all memberships
|
# remove all memberships
|
||||||
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
|
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
|
||||||
@@ -75,6 +82,8 @@ def _delete_course_group(location):
|
|||||||
This is to be called only by either a command line code path or through an app which has already
|
This is to be called only by either a command line code path or through an app which has already
|
||||||
asserted permissions to do this action
|
asserted permissions to do this action
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def _copy_course_group(source, dest):
|
def _copy_course_group(source, dest):
|
||||||
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
|
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
|
||||||
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
|
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
|
||||||
@@ -86,7 +95,7 @@ def _copy_course_group(source, dest):
|
|||||||
new_staff_group = Group.objects.get(name=get_course_groupname_for_role(dest, STAFF_ROLE_NAME))
|
new_staff_group = Group.objects.get(name=get_course_groupname_for_role(dest, STAFF_ROLE_NAME))
|
||||||
for user in staff.user_set.all():
|
for user in staff.user_set.all():
|
||||||
user.groups.add(new_staff_group)
|
user.groups.add(new_staff_group)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
def add_user_to_course_group(caller, user, location, role):
|
def add_user_to_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
|
|||||||
|
|
||||||
## 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:
|
||||||
@@ -21,13 +23,13 @@ def get_course_updates(location):
|
|||||||
|
|
||||||
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
|
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
|
||||||
location_base = course_updates.location.url()
|
location_base = course_updates.location.url()
|
||||||
|
|
||||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||||
try:
|
try:
|
||||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||||
except:
|
except:
|
||||||
course_html_parsed = html.fromstring("<ol></ol>")
|
course_html_parsed = html.fromstring("<ol></ol>")
|
||||||
|
|
||||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||||
course_upd_collection = []
|
course_upd_collection = []
|
||||||
if course_html_parsed.tag == 'ol':
|
if course_html_parsed.tag == 'ol':
|
||||||
@@ -40,25 +42,26 @@ def get_course_updates(location):
|
|||||||
content = update[0].tail
|
content = update[0].tail
|
||||||
else:
|
else:
|
||||||
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
|
||||||
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
|
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
|
||||||
into the html structure.
|
into the html structure.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
course_updates = modulestore('direct').get_item(location)
|
course_updates = modulestore('direct').get_item(location)
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
|
|
||||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||||
try:
|
try:
|
||||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||||
@@ -67,7 +70,7 @@ def update_course_updates(location, update, passed_id=None):
|
|||||||
|
|
||||||
# No try/catch b/c failure generates an error back to client
|
# No try/catch b/c failure generates an error back to client
|
||||||
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
|
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
|
||||||
|
|
||||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||||
if course_html_parsed.tag == 'ol':
|
if course_html_parsed.tag == 'ol':
|
||||||
# ??? Should this use the id in the json or in the url or does it matter?
|
# ??? Should this use the id in the json or in the url or does it matter?
|
||||||
@@ -80,14 +83,15 @@ def update_course_updates(location, update, passed_id=None):
|
|||||||
|
|
||||||
idx = len(course_html_parsed)
|
idx = len(course_html_parsed)
|
||||||
passed_id = course_updates.location.url() + "/" + str(idx)
|
passed_id = course_updates.location.url() + "/" + str(idx)
|
||||||
|
|
||||||
# update db record
|
# update db record
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@@ -96,19 +100,19 @@ def delete_course_update(location, update, passed_id):
|
|||||||
"""
|
"""
|
||||||
if not passed_id:
|
if not passed_id:
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
|
|
||||||
try:
|
try:
|
||||||
course_updates = modulestore('direct').get_item(location)
|
course_updates = modulestore('direct').get_item(location)
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
|
|
||||||
# TODO use delete_blank_text parser throughout and cache as a static var in a class
|
# TODO use delete_blank_text parser throughout and cache as a static var in a class
|
||||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||||
try:
|
try:
|
||||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||||
except:
|
except:
|
||||||
course_html_parsed = html.fromstring("<ol></ol>")
|
course_html_parsed = html.fromstring("<ol></ol>")
|
||||||
|
|
||||||
if course_html_parsed.tag == 'ol':
|
if course_html_parsed.tag == 'ol':
|
||||||
# ??? Should this use the id in the json or in the url or does it matter?
|
# ??? Should this use the id in the json or in the url or does it matter?
|
||||||
idx = get_idx(passed_id)
|
idx = get_idx(passed_id)
|
||||||
@@ -120,10 +124,11 @@ def delete_course_update(location, update, passed_id):
|
|||||||
# update db record
|
# update db record
|
||||||
course_updates.definition['data'] = html.tostring(course_html_parsed)
|
course_updates.definition['data'] = html.tostring(course_html_parsed)
|
||||||
store = modulestore('direct')
|
store = modulestore('direct')
|
||||||
store.update_item(location, course_updates.definition['data'])
|
store.update_item(location, course_updates.definition['data'])
|
||||||
|
|
||||||
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.
|
||||||
@@ -131,4 +136,4 @@ def get_idx(passed_id):
|
|||||||
# TODO compile this regex into a class static and reuse for each call
|
# TODO compile this regex into a class static and reuse for each call
|
||||||
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
|
idx_matcher = re.search(r'.*/(\d+)$', passed_id)
|
||||||
if idx_matcher:
|
if idx_matcher:
|
||||||
return int(idx_matcher.group(1))
|
return int(idx_matcher.group(1))
|
||||||
|
|||||||
@@ -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,32 +22,37 @@ 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',
|
||||||
password='test',
|
password='test',
|
||||||
is_staff=False):
|
is_staff=False):
|
||||||
studio_user = UserFactory.build(
|
studio_user = UserFactory.build(
|
||||||
username=uname,
|
username=uname,
|
||||||
email=email,
|
email=email,
|
||||||
password=password,
|
password=password,
|
||||||
is_staff=is_staff)
|
is_staff=is_staff)
|
||||||
@@ -58,6 +65,7 @@ def create_studio_user(
|
|||||||
|
|
||||||
user_profile = UserProfileFactory(user=studio_user)
|
user_profile = UserProfileFactory(user=studio_user)
|
||||||
|
|
||||||
|
|
||||||
def flush_xmodule_store():
|
def flush_xmodule_store():
|
||||||
# Flush and initialize the module store
|
# Flush and initialize the module store
|
||||||
# It needs the templates because it creates new records
|
# It needs the templates because it creates new records
|
||||||
@@ -70,26 +78,32 @@ def flush_xmodule_store():
|
|||||||
xmodule.modulestore.django.modulestore().collection.drop()
|
xmodule.modulestore.django.modulestore().collection.drop()
|
||||||
xmodule.templates.update_templates()
|
xmodule.templates.update_templates()
|
||||||
|
|
||||||
def assert_css_with_text(css,text):
|
|
||||||
|
def assert_css_with_text(css, text):
|
||||||
assert_true(world.browser.is_element_present_by_css(css, 5))
|
assert_true(world.browser.is_element_present_by_css(css, 5))
|
||||||
assert_equal(world.browser.find_by_css(css).text, text)
|
assert_equal(world.browser.find_by_css(css).text, text)
|
||||||
|
|
||||||
|
|
||||||
def css_click(css):
|
def css_click(css):
|
||||||
world.browser.find_by_css(css).first.click()
|
world.browser.find_by_css(css).first.click()
|
||||||
|
|
||||||
|
|
||||||
def css_fill(css, value):
|
def css_fill(css, value):
|
||||||
world.browser.find_by_css(css).first.fill(value)
|
world.browser.find_by_css(css).first.fill(value)
|
||||||
|
|
||||||
|
|
||||||
def clear_courses():
|
def clear_courses():
|
||||||
flush_xmodule_store()
|
flush_xmodule_store()
|
||||||
|
|
||||||
|
|
||||||
def fill_in_course_info(
|
def fill_in_course_info(
|
||||||
name='Robot Super Course',
|
name='Robot Super Course',
|
||||||
org='MITx',
|
org='MITx',
|
||||||
num='101'):
|
num='101'):
|
||||||
css_fill('.new-course-name',name)
|
css_fill('.new-course-name', name)
|
||||||
css_fill('.new-course-org',org)
|
css_fill('.new-course-org', org)
|
||||||
css_fill('.new-course-number',num)
|
css_fill('.new-course-number', num)
|
||||||
|
|
||||||
|
|
||||||
def log_into_studio(
|
def log_into_studio(
|
||||||
uname='robot',
|
uname='robot',
|
||||||
@@ -108,24 +122,27 @@ 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)
|
||||||
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, name)
|
css_fill(name_css, name)
|
||||||
css_click(save_css)
|
css_click(save_css)
|
||||||
|
|||||||
@@ -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
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class UserProfileFactory(factory.Factory):
|
class UserProfileFactory(factory.Factory):
|
||||||
FACTORY_FOR = UserProfile
|
FACTORY_FOR = UserProfile
|
||||||
|
|
||||||
@@ -10,12 +11,14 @@ class UserProfileFactory(factory.Factory):
|
|||||||
name = 'Robot Studio'
|
name = 'Robot Studio'
|
||||||
courseware = 'course.xml'
|
courseware = 'course.xml'
|
||||||
|
|
||||||
|
|
||||||
class RegistrationFactory(factory.Factory):
|
class RegistrationFactory(factory.Factory):
|
||||||
FACTORY_FOR = Registration
|
FACTORY_FOR = Registration
|
||||||
|
|
||||||
user = None
|
user = None
|
||||||
activation_key = uuid.uuid4().hex
|
activation_key = uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(factory.Factory):
|
class UserFactory(factory.Factory):
|
||||||
FACTORY_FOR = User
|
FACTORY_FOR = User
|
||||||
|
|
||||||
@@ -28,4 +31,4 @@ class UserFactory(factory.Factory):
|
|||||||
is_active = True
|
is_active = True
|
||||||
is_superuser = False
|
is_superuser = False
|
||||||
last_login = datetime.now()
|
last_login = datetime.now()
|
||||||
date_joined = datetime.now()
|
date_joined = datetime.now()
|
||||||
|
|||||||
@@ -2,54 +2,65 @@ from lettuce import world, step
|
|||||||
from common import *
|
from common import *
|
||||||
|
|
||||||
############### ACTIONS ####################
|
############### ACTIONS ####################
|
||||||
|
|
||||||
|
|
||||||
@step('I have opened a new course in Studio$')
|
@step('I have opened a new course in Studio$')
|
||||||
def i_have_opened_a_new_course(step):
|
def i_have_opened_a_new_course(step):
|
||||||
clear_courses()
|
clear_courses()
|
||||||
log_into_studio()
|
log_into_studio()
|
||||||
create_a_course()
|
create_a_course()
|
||||||
|
|
||||||
|
|
||||||
@step('I click the new section link$')
|
@step('I click the new section link$')
|
||||||
def i_click_new_section_link(step):
|
def i_click_new_section_link(step):
|
||||||
link_css = 'a.new-courseware-section-button'
|
link_css = 'a.new-courseware-section-button'
|
||||||
css_click(link_css)
|
css_click(link_css)
|
||||||
|
|
||||||
|
|
||||||
@step('I enter the section name and click save$')
|
@step('I enter the section name and click save$')
|
||||||
def i_save_section_name(step):
|
def i_save_section_name(step):
|
||||||
name_css = '.new-section-name'
|
name_css = '.new-section-name'
|
||||||
save_css = '.new-section-name-save'
|
save_css = '.new-section-name-save'
|
||||||
css_fill(name_css,'My Section')
|
css_fill(name_css, 'My Section')
|
||||||
css_click(save_css)
|
css_click(save_css)
|
||||||
|
|
||||||
|
|
||||||
@step('I have added a new section$')
|
@step('I have added a new section$')
|
||||||
def i_have_added_new_section(step):
|
def i_have_added_new_section(step):
|
||||||
add_section()
|
add_section()
|
||||||
|
|
||||||
|
|
||||||
@step('I click the Edit link for the release date$')
|
@step('I click the Edit link for the release date$')
|
||||||
def i_click_the_edit_link_for_the_release_date(step):
|
def i_click_the_edit_link_for_the_release_date(step):
|
||||||
button_css = 'div.section-published-date a.edit-button'
|
button_css = 'div.section-published-date a.edit-button'
|
||||||
css_click(button_css)
|
css_click(button_css)
|
||||||
|
|
||||||
|
|
||||||
@step('I save a new section release date$')
|
@step('I save a new section release date$')
|
||||||
def i_save_a_new_section_release_date(step):
|
def i_save_a_new_section_release_date(step):
|
||||||
date_css = 'input.start-date.date.hasDatepicker'
|
date_css = 'input.start-date.date.hasDatepicker'
|
||||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||||
css_fill(date_css,'12/25/2013')
|
css_fill(date_css, '12/25/2013')
|
||||||
# click here to make the calendar go away
|
# click here to make the calendar go away
|
||||||
css_click(time_css)
|
css_click(time_css)
|
||||||
css_fill(time_css,'12:00am')
|
css_fill(time_css, '12:00am')
|
||||||
css_click('a.save-button')
|
css_click('a.save-button')
|
||||||
|
|
||||||
############ ASSERTIONS ###################
|
############ ASSERTIONS ###################
|
||||||
|
|
||||||
|
|
||||||
@step('I see my section on the Courseware page$')
|
@step('I see my section on the Courseware page$')
|
||||||
def i_see_my_section_on_the_courseware_page(step):
|
def i_see_my_section_on_the_courseware_page(step):
|
||||||
section_css = 'span.section-name-span'
|
section_css = 'span.section-name-span'
|
||||||
assert_css_with_text(section_css,'My Section')
|
assert_css_with_text(section_css, 'My Section')
|
||||||
|
|
||||||
|
|
||||||
@step('the section does not exist$')
|
@step('the section does not exist$')
|
||||||
def section_does_not_exist(step):
|
def section_does_not_exist(step):
|
||||||
css = 'span.section-name-span'
|
css = 'span.section-name-span'
|
||||||
assert world.browser.is_element_not_present_by_css(css)
|
assert world.browser.is_element_not_present_by_css(css)
|
||||||
|
|
||||||
|
|
||||||
@step('I see a release date for my section$')
|
@step('I see a release date for my section$')
|
||||||
def i_see_a_release_date_for_my_section(step):
|
def i_see_a_release_date_for_my_section(step):
|
||||||
import re
|
import re
|
||||||
@@ -63,18 +74,21 @@ def i_see_a_release_date_for_my_section(step):
|
|||||||
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]'
|
date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]'
|
||||||
time_regex = '[0-2][0-9]:[0-5][0-9]'
|
time_regex = '[0-2][0-9]:[0-5][0-9]'
|
||||||
match_string = '%s %s at %s' % (msg, date_regex, time_regex)
|
match_string = '%s %s at %s' % (msg, date_regex, time_regex)
|
||||||
assert re.match(match_string,status_text)
|
assert re.match(match_string, status_text)
|
||||||
|
|
||||||
|
|
||||||
@step('I see a link to create a new subsection$')
|
@step('I see a link to create a new subsection$')
|
||||||
def i_see_a_link_to_create_a_new_subsection(step):
|
def i_see_a_link_to_create_a_new_subsection(step):
|
||||||
css = 'a.new-subsection-item'
|
css = 'a.new-subsection-item'
|
||||||
assert world.browser.is_element_present_by_css(css)
|
assert world.browser.is_element_present_by_css(css)
|
||||||
|
|
||||||
|
|
||||||
@step('the section release date picker is not visible$')
|
@step('the section release date picker is not visible$')
|
||||||
def the_section_release_date_picker_not_visible(step):
|
def the_section_release_date_picker_not_visible(step):
|
||||||
css = 'div.edit-subsection-publish-settings'
|
css = 'div.edit-subsection-publish-settings'
|
||||||
assert False, world.browser.find_by_css(css).visible
|
assert False, world.browser.find_by_css(css).visible
|
||||||
|
|
||||||
|
|
||||||
@step('the section release date is updated$')
|
@step('the section release date is updated$')
|
||||||
def the_section_release_date_is_updated(step):
|
def the_section_release_date_is_updated(step):
|
||||||
css = 'span.published-status'
|
css = 'span.published-status'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from lettuce import world, step
|
from lettuce import world, step
|
||||||
|
|
||||||
|
|
||||||
@step('I fill in the registration form$')
|
@step('I fill in the registration form$')
|
||||||
def i_fill_in_the_registration_form(step):
|
def i_fill_in_the_registration_form(step):
|
||||||
register_form = world.browser.find_by_css('form#register_form')
|
register_form = world.browser.find_by_css('form#register_form')
|
||||||
@@ -9,15 +10,18 @@ def i_fill_in_the_registration_form(step):
|
|||||||
register_form.find_by_name('name').fill('Robot Studio')
|
register_form.find_by_name('name').fill('Robot Studio')
|
||||||
register_form.find_by_name('terms_of_service').check()
|
register_form.find_by_name('terms_of_service').check()
|
||||||
|
|
||||||
|
|
||||||
@step('I press the "([^"]*)" button on the registration form$')
|
@step('I press the "([^"]*)" button on the registration form$')
|
||||||
def i_press_the_button_on_the_registration_form(step, button):
|
def i_press_the_button_on_the_registration_form(step, button):
|
||||||
register_form = world.browser.find_by_css('form#register_form')
|
register_form = world.browser.find_by_css('form#register_form')
|
||||||
register_form.find_by_value(button).click()
|
register_form.find_by_value(button).click()
|
||||||
|
|
||||||
|
|
||||||
@step('I should see be on the studio home page$')
|
@step('I should see be on the studio home page$')
|
||||||
def i_should_see_be_on_the_studio_home_page(step):
|
def i_should_see_be_on_the_studio_home_page(step):
|
||||||
assert world.browser.find_by_css('div.inner-wrapper')
|
assert world.browser.find_by_css('div.inner-wrapper')
|
||||||
|
|
||||||
|
|
||||||
@step(u'I should see the message "([^"]*)"$')
|
@step(u'I should see the message "([^"]*)"$')
|
||||||
def i_should_see_the_message(step, msg):
|
def i_should_see_the_message(step, msg):
|
||||||
assert world.browser.is_text_present(msg, 5)
|
assert world.browser.is_text_present(msg, 5)
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ from nose.tools import assert_true, assert_false, assert_equal
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I have a course with no sections$')
|
@step(u'I have a course with no sections$')
|
||||||
def have_a_course(step):
|
def have_a_course(step):
|
||||||
clear_courses()
|
clear_courses()
|
||||||
course = CourseFactory.create()
|
course = CourseFactory.create()
|
||||||
|
|
||||||
|
|
||||||
@step(u'I have a course with 1 section$')
|
@step(u'I have a course with 1 section$')
|
||||||
def have_a_course_with_1_section(step):
|
def have_a_course_with_1_section(step):
|
||||||
clear_courses()
|
clear_courses()
|
||||||
@@ -18,8 +20,9 @@ def have_a_course_with_1_section(step):
|
|||||||
section = ItemFactory.create(parent_location=course.location)
|
section = ItemFactory.create(parent_location=course.location)
|
||||||
subsection1 = ItemFactory.create(
|
subsection1 = ItemFactory.create(
|
||||||
parent_location=section.location,
|
parent_location=section.location,
|
||||||
template = 'i4x://edx/templates/sequential/Empty',
|
template='i4x://edx/templates/sequential/Empty',
|
||||||
display_name = 'Subsection One',)
|
display_name='Subsection One',)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I have a course with multiple sections$')
|
@step(u'I have a course with multiple sections$')
|
||||||
def have_a_course_with_two_sections(step):
|
def have_a_course_with_two_sections(step):
|
||||||
@@ -28,19 +31,20 @@ def have_a_course_with_two_sections(step):
|
|||||||
section = ItemFactory.create(parent_location=course.location)
|
section = ItemFactory.create(parent_location=course.location)
|
||||||
subsection1 = ItemFactory.create(
|
subsection1 = ItemFactory.create(
|
||||||
parent_location=section.location,
|
parent_location=section.location,
|
||||||
template = 'i4x://edx/templates/sequential/Empty',
|
template='i4x://edx/templates/sequential/Empty',
|
||||||
display_name = 'Subsection One',)
|
display_name='Subsection One',)
|
||||||
section2 = ItemFactory.create(
|
section2 = ItemFactory.create(
|
||||||
parent_location=course.location,
|
parent_location=course.location,
|
||||||
display_name='Section Two',)
|
display_name='Section Two',)
|
||||||
subsection2 = ItemFactory.create(
|
subsection2 = ItemFactory.create(
|
||||||
parent_location=section2.location,
|
parent_location=section2.location,
|
||||||
template = 'i4x://edx/templates/sequential/Empty',
|
template='i4x://edx/templates/sequential/Empty',
|
||||||
display_name = 'Subsection Alpha',)
|
display_name='Subsection Alpha',)
|
||||||
subsection3 = ItemFactory.create(
|
subsection3 = ItemFactory.create(
|
||||||
parent_location=section2.location,
|
parent_location=section2.location,
|
||||||
template = 'i4x://edx/templates/sequential/Empty',
|
template='i4x://edx/templates/sequential/Empty',
|
||||||
display_name = 'Subsection Beta',)
|
display_name='Subsection Beta',)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I navigate to the course overview page$')
|
@step(u'I navigate to the course overview page$')
|
||||||
def navigate_to_the_course_overview_page(step):
|
def navigate_to_the_course_overview_page(step):
|
||||||
@@ -48,15 +52,18 @@ def navigate_to_the_course_overview_page(step):
|
|||||||
course_locator = '.class-name'
|
course_locator = '.class-name'
|
||||||
css_click(course_locator)
|
css_click(course_locator)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I navigate to the courseware page of a course with multiple sections')
|
@step(u'I navigate to the courseware page of a course with multiple sections')
|
||||||
def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step):
|
def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step):
|
||||||
step.given('I have a course with multiple sections')
|
step.given('I have a course with multiple sections')
|
||||||
step.given('I navigate to the course overview page')
|
step.given('I navigate to the course overview page')
|
||||||
|
|
||||||
|
|
||||||
@step(u'I add a section')
|
@step(u'I add a section')
|
||||||
def i_add_a_section(step):
|
def i_add_a_section(step):
|
||||||
add_section(name='My New Section That I Just Added')
|
add_section(name='My New Section That I Just Added')
|
||||||
|
|
||||||
|
|
||||||
@step(u'I click the "([^"]*)" link$')
|
@step(u'I click the "([^"]*)" link$')
|
||||||
def i_click_the_text_span(step, text):
|
def i_click_the_text_span(step, text):
|
||||||
span_locator = '.toggle-button-sections span'
|
span_locator = '.toggle-button-sections span'
|
||||||
@@ -65,16 +72,19 @@ def i_click_the_text_span(step, text):
|
|||||||
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
||||||
css_click(span_locator)
|
css_click(span_locator)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I collapse the first section$')
|
@step(u'I collapse the first section$')
|
||||||
def i_collapse_a_section(step):
|
def i_collapse_a_section(step):
|
||||||
collapse_locator = 'section.courseware-section a.collapse'
|
collapse_locator = 'section.courseware-section a.collapse'
|
||||||
css_click(collapse_locator)
|
css_click(collapse_locator)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I expand the first section$')
|
@step(u'I expand the first section$')
|
||||||
def i_expand_a_section(step):
|
def i_expand_a_section(step):
|
||||||
expand_locator = 'section.courseware-section a.expand'
|
expand_locator = 'section.courseware-section a.expand'
|
||||||
css_click(expand_locator)
|
css_click(expand_locator)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I see the "([^"]*)" link$')
|
@step(u'I see the "([^"]*)" link$')
|
||||||
def i_see_the_span_with_text(step, text):
|
def i_see_the_span_with_text(step, text):
|
||||||
span_locator = '.toggle-button-sections span'
|
span_locator = '.toggle-button-sections span'
|
||||||
@@ -82,6 +92,7 @@ def i_see_the_span_with_text(step, text):
|
|||||||
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
assert_equal(world.browser.find_by_css(span_locator).value, text)
|
||||||
assert_true(world.browser.find_by_css(span_locator).visible)
|
assert_true(world.browser.find_by_css(span_locator).visible)
|
||||||
|
|
||||||
|
|
||||||
@step(u'I do not see the "([^"]*)" link$')
|
@step(u'I do not see the "([^"]*)" link$')
|
||||||
def i_do_not_see_the_span_with_text(step, text):
|
def i_do_not_see_the_span_with_text(step, text):
|
||||||
# Note that the span will exist on the page but not be visible
|
# Note that the span will exist on the page but not be visible
|
||||||
@@ -89,6 +100,7 @@ def i_do_not_see_the_span_with_text(step, text):
|
|||||||
assert_true(world.browser.is_element_present_by_css(span_locator))
|
assert_true(world.browser.is_element_present_by_css(span_locator))
|
||||||
assert_false(world.browser.find_by_css(span_locator).visible)
|
assert_false(world.browser.find_by_css(span_locator).visible)
|
||||||
|
|
||||||
|
|
||||||
@step(u'all sections are expanded$')
|
@step(u'all sections are expanded$')
|
||||||
def all_sections_are_expanded(step):
|
def all_sections_are_expanded(step):
|
||||||
subsection_locator = 'div.subsection-list'
|
subsection_locator = 'div.subsection-list'
|
||||||
@@ -96,9 +108,10 @@ 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'
|
||||||
subsections = world.browser.find_by_css(subsection_locator)
|
subsections = world.browser.find_by_css(subsection_locator)
|
||||||
for s in subsections:
|
for s in subsections:
|
||||||
assert_false(s.visible)
|
assert_false(s.visible)
|
||||||
|
|||||||
@@ -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,31 +11,37 @@ 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):
|
||||||
css = 'span.subsection-name'
|
css = 'span.subsection-name'
|
||||||
assert world.browser.is_element_not_present_by_css(css)
|
assert world.browser.is_element_not_present_by_css(css)
|
||||||
|
|||||||
@@ -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
|
|||||||
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
|
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = \
|
help = \
|
||||||
'''Delete a MongoDB backed course'''
|
'''Delete a MongoDB backed course'''
|
||||||
@@ -35,6 +36,3 @@ class Command(BaseCommand):
|
|||||||
print 'removing User permissions from course....'
|
print 'removing User permissions from course....'
|
||||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||||
_delete_course_group(loc)
|
_delete_course_group(loc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def query_yes_no(question, default="yes"):
|
def query_yes_no(question, default="yes"):
|
||||||
"""Ask a yes/no question via raw_input() and return their answer.
|
"""Ask a yes/no question via raw_input() and return their answer.
|
||||||
|
|
||||||
@@ -30,4 +31,4 @@ def query_yes_no(question, default="yes"):
|
|||||||
return valid[choice]
|
return valid[choice]
|
||||||
else:
|
else:
|
||||||
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
||||||
"(or 'y' or 'n').\n")
|
"(or 'y' or 'n').\n")
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from lxml import etree
|
|||||||
import re
|
import re
|
||||||
from django.http import HttpResponseBadRequest, Http404
|
from django.http import HttpResponseBadRequest, Http404
|
||||||
|
|
||||||
def get_module_info(store, location, parent_location = None, rewrite_static_links = False):
|
|
||||||
|
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
|
||||||
try:
|
try:
|
||||||
if location.revision is None:
|
if location.revision is None:
|
||||||
module = store.get_item(location)
|
module = store.get_item(location)
|
||||||
@@ -36,6 +37,7 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link
|
|||||||
'metadata': module.metadata
|
'metadata': module.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def set_module_info(store, location, post_data):
|
def set_module_info(store, location, post_data):
|
||||||
module = None
|
module = None
|
||||||
isNew = False
|
isNew = False
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from student.models import (User, UserProfile, Registration,
|
|||||||
CourseEnrollmentAllowed)
|
CourseEnrollmentAllowed)
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
|
||||||
class UserProfileFactory(Factory):
|
class UserProfileFactory(Factory):
|
||||||
FACTORY_FOR = UserProfile
|
FACTORY_FOR = UserProfile
|
||||||
|
|
||||||
@@ -12,12 +13,14 @@ class UserProfileFactory(Factory):
|
|||||||
name = 'Robot Studio'
|
name = 'Robot Studio'
|
||||||
courseware = 'course.xml'
|
courseware = 'course.xml'
|
||||||
|
|
||||||
|
|
||||||
class RegistrationFactory(Factory):
|
class RegistrationFactory(Factory):
|
||||||
FACTORY_FOR = Registration
|
FACTORY_FOR = Registration
|
||||||
|
|
||||||
user = None
|
user = None
|
||||||
activation_key = uuid4().hex
|
activation_key = uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(Factory):
|
class UserFactory(Factory):
|
||||||
FACTORY_FOR = User
|
FACTORY_FOR = User
|
||||||
|
|
||||||
@@ -32,11 +35,13 @@ class UserFactory(Factory):
|
|||||||
last_login = datetime.now()
|
last_login = datetime.now()
|
||||||
date_joined = datetime.now()
|
date_joined = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class GroupFactory(Factory):
|
class GroupFactory(Factory):
|
||||||
FACTORY_FOR = Group
|
FACTORY_FOR = Group
|
||||||
|
|
||||||
name = 'test_group'
|
name = 'test_group'
|
||||||
|
|
||||||
|
|
||||||
class CourseEnrollmentAllowedFactory(Factory):
|
class CourseEnrollmentAllowedFactory(Factory):
|
||||||
FACTORY_FOR = CourseEnrollmentAllowed
|
FACTORY_FOR = CourseEnrollmentAllowed
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
|||||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||||
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||||
|
|
||||||
|
|
||||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||||
"""
|
"""
|
||||||
@@ -77,20 +78,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
def test_static_tab_reordering(self):
|
def test_static_tab_reordering(self):
|
||||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||||
|
|
||||||
ms = modulestore('direct')
|
ms = modulestore('direct')
|
||||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||||
|
|
||||||
# reverse the ordering
|
# reverse the ordering
|
||||||
reverse_tabs = []
|
reverse_tabs = []
|
||||||
for tab in course.tabs:
|
for tab in course.tabs:
|
||||||
if tab['type'] == 'static_tab':
|
if tab['type'] == 'static_tab':
|
||||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||||
|
|
||||||
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json")
|
|
||||||
|
|
||||||
course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
|
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
|
||||||
|
|
||||||
|
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||||
|
|
||||||
# compare to make sure that the tabs information is in the expected order after the server call
|
# compare to make sure that the tabs information is in the expected order after the server call
|
||||||
course_tabs = []
|
course_tabs = []
|
||||||
for tab in course.tabs:
|
for tab in course.tabs:
|
||||||
@@ -101,17 +102,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
def test_about_overrides(self):
|
def test_about_overrides(self):
|
||||||
'''
|
'''
|
||||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||||
while there is a base definition in /about/effort.html
|
while there is a base definition in /about/effort.html
|
||||||
'''
|
'''
|
||||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||||
ms = modulestore('direct')
|
ms = modulestore('direct')
|
||||||
effort = ms.get_item(Location(['i4x','edX','full','about','effort', None]))
|
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||||
self.assertEqual(effort.definition['data'],'6 hours')
|
self.assertEqual(effort.definition['data'], '6 hours')
|
||||||
|
|
||||||
# this one should be in a non-override folder
|
# this one should be in a non-override folder
|
||||||
effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None]))
|
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||||
self.assertEqual(effort.definition['data'],'TBD')
|
self.assertEqual(effort.definition['data'], 'TBD')
|
||||||
|
|
||||||
def test_remove_hide_progress_tab(self):
|
def test_remove_hide_progress_tab(self):
|
||||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||||
@@ -147,14 +148,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
clone_course(ms, cs, source_location, dest_location)
|
clone_course(ms, cs, source_location, dest_location)
|
||||||
|
|
||||||
# now loop through all the units in the course and verify that the clone can render them, which
|
# now loop through all the units in the course and verify that the clone can render them, which
|
||||||
# means the objects are at least present
|
# means the objects are at least present
|
||||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||||
self.assertGreater(len(items), 0)
|
self.assertGreater(len(items), 0)
|
||||||
clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None]))
|
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||||
self.assertGreater(len(clone_items), 0)
|
self.assertGreater(len(clone_items), 0)
|
||||||
for descriptor in items:
|
for descriptor in items:
|
||||||
new_loc = descriptor.location._replace(org = 'MITx', course='999')
|
new_loc = descriptor.location._replace(org='MITx', course='999')
|
||||||
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
|
||||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
@@ -169,7 +170,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
delete_course(ms, cs, location)
|
delete_course(ms, cs, location)
|
||||||
|
|
||||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||||
self.assertEqual(len(items), 0)
|
self.assertEqual(len(items), 0)
|
||||||
|
|
||||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||||
@@ -185,7 +186,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
def test_export_course(self):
|
def test_export_course(self):
|
||||||
ms = modulestore('direct')
|
ms = modulestore('direct')
|
||||||
cs = contentstore()
|
cs = contentstore()
|
||||||
|
|
||||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||||
@@ -205,7 +206,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
# check for custom_tags
|
# check for custom_tags
|
||||||
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
|
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||||
|
|
||||||
|
|
||||||
# remove old course
|
# remove old course
|
||||||
delete_course(ms, cs, location)
|
delete_course(ms, cs, location)
|
||||||
@@ -213,23 +214,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
# reimport
|
# reimport
|
||||||
import_from_xml(ms, root_dir, ['test_export'])
|
import_from_xml(ms, root_dir, ['test_export'])
|
||||||
|
|
||||||
items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
|
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||||
self.assertGreater(len(items), 0)
|
self.assertGreater(len(items), 0)
|
||||||
for descriptor in items:
|
for descriptor in items:
|
||||||
print "Checking {0}....".format(descriptor.location.url())
|
print "Checking {0}....".format(descriptor.location.url())
|
||||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
shutil.rmtree(root_dir)
|
shutil.rmtree(root_dir)
|
||||||
|
|
||||||
def test_course_handouts_rewrites(self):
|
def test_course_handouts_rewrites(self):
|
||||||
ms = modulestore('direct')
|
ms = modulestore('direct')
|
||||||
cs = contentstore()
|
cs = contentstore()
|
||||||
|
|
||||||
# import a test course
|
# import a test course
|
||||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||||
|
|
||||||
handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
||||||
|
|
||||||
# get module info
|
# get module info
|
||||||
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
|
resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
|
||||||
@@ -239,7 +240,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
# check that /static/ has been converted to the full path
|
# check that /static/ has been converted to the full path
|
||||||
# note, we know the link it should be because that's what in the 'full' course in the test data
|
# note, we know the link it should be because that's what in the 'full' course in the test data
|
||||||
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
|
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
|
||||||
|
|
||||||
|
|
||||||
class ContentStoreTest(ModuleStoreTestCase):
|
class ContentStoreTest(ModuleStoreTestCase):
|
||||||
@@ -302,7 +303,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
data = parse_json(resp)
|
data = parse_json(resp)
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(data['ErrMsg'],
|
self.assertEqual(data['ErrMsg'],
|
||||||
'There is already a course defined with the same organization and course number.')
|
'There is already a course defined with the same organization and course number.')
|
||||||
|
|
||||||
def test_create_course_with_bad_organization(self):
|
def test_create_course_with_bad_organization(self):
|
||||||
@@ -319,7 +320,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
"""Test viewing the index page with no courses"""
|
"""Test viewing the index page with no courses"""
|
||||||
# Create a course so there is something to view
|
# Create a course so there is something to view
|
||||||
resp = self.client.get(reverse('index'))
|
resp = self.client.get(reverse('index'))
|
||||||
self.assertContains(resp,
|
self.assertContains(resp,
|
||||||
'<h1>My Courses</h1>',
|
'<h1>My Courses</h1>',
|
||||||
status_code=200,
|
status_code=200,
|
||||||
html=True)
|
html=True)
|
||||||
@@ -355,7 +356,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.get(reverse('course_index', kwargs=data))
|
resp = self.client.get(reverse('course_index', kwargs=data))
|
||||||
self.assertContains(resp,
|
self.assertContains(resp,
|
||||||
'<a href="/MITx/999/course/Robot_Super_Course" class="class-name">Robot Super Course</a>',
|
'<a href="/MITx/999/course/Robot_Super_Course" class="class-name">Robot Super Course</a>',
|
||||||
status_code=200,
|
status_code=200,
|
||||||
html=True)
|
html=True)
|
||||||
@@ -365,8 +366,8 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||||
|
|
||||||
section_data = {
|
section_data = {
|
||||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||||
'template' : 'i4x://edx/templates/chapter/Empty',
|
'template': 'i4x://edx/templates/chapter/Empty',
|
||||||
'display_name': 'Section One',
|
'display_name': 'Section One',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +375,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
data = parse_json(resp)
|
data = parse_json(resp)
|
||||||
self.assertRegexpMatches(data['id'],
|
self.assertRegexpMatches(data['id'],
|
||||||
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||||
|
|
||||||
def test_capa_module(self):
|
def test_capa_module(self):
|
||||||
@@ -382,8 +383,8 @@ class ContentStoreTest(ModuleStoreTestCase):
|
|||||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||||
|
|
||||||
problem_data = {
|
problem_data = {
|
||||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||||
'template' : 'i4x://edx/templates/problem/Empty'
|
'template': 'i4x://edx/templates/problem/Empty'
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = self.client.post(reverse('clone_item'), problem_data)
|
resp = self.client.post(reverse('clone_item'), problem_data)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from xmodule.modulestore import Location
|
|||||||
from xmodule.contentstore.content import StaticContent
|
from xmodule.contentstore.content import StaticContent
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class Content:
|
class Content:
|
||||||
def __init__(self, location, content):
|
def __init__(self, location, content):
|
||||||
self.location = location
|
self.location = location
|
||||||
@@ -11,6 +12,7 @@ class Content:
|
|||||||
def get_id(self):
|
def get_id(self):
|
||||||
return StaticContent.get_id_from_location(self.location)
|
return StaticContent.get_id_from_location(self.location)
|
||||||
|
|
||||||
|
|
||||||
class CachingTestCase(TestCase):
|
class CachingTestCase(TestCase):
|
||||||
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy
|
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy
|
||||||
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg')
|
unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg')
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
|||||||
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)
|
||||||
dt2 = ConvertersTestCase.struct_to_datetime(date2)
|
dt2 = ConvertersTestCase.struct_to_datetime(date2)
|
||||||
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta))
|
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta))
|
||||||
|
|
||||||
def test_iso_to_struct(self):
|
def test_iso_to_struct(self):
|
||||||
self.compare_dates(converters.jsdate_to_time("2013-01-01"), converters.jsdate_to_time("2012-12-31"), datetime.timedelta(days=1))
|
self.compare_dates(converters.jsdate_to_time("2013-01-01"), converters.jsdate_to_time("2012-12-31"), datetime.timedelta(days=1))
|
||||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), converters.jsdate_to_time("2012-12-31T23"), datetime.timedelta(hours=1))
|
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), converters.jsdate_to_time("2012-12-31T23"), datetime.timedelta(hours=1))
|
||||||
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1))
|
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=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))
|
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(ModuleStoreTestCase):
|
class CourseTestCase(ModuleStoreTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
@@ -68,13 +68,14 @@ class CourseTestCase(ModuleStoreTestCase):
|
|||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.client.login(username=uname, password=password)
|
self.client.login(username=uname, password=password)
|
||||||
|
|
||||||
t='i4x://edx/templates/course/Empty'
|
t = 'i4x://edx/templates/course/Empty'
|
||||||
o='MITx'
|
o = 'MITx'
|
||||||
n='999'
|
n = '999'
|
||||||
dn='Robot Super Course'
|
dn = 'Robot Super Course'
|
||||||
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
|
self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
|
||||||
CourseFactory.create(template=t, org=o, number=n, display_name=dn)
|
CourseFactory.create(template=t, org=o, number=n, display_name=dn)
|
||||||
|
|
||||||
|
|
||||||
class CourseDetailsTestCase(CourseTestCase):
|
class CourseDetailsTestCase(CourseTestCase):
|
||||||
def test_virgin_fetch(self):
|
def test_virgin_fetch(self):
|
||||||
details = CourseDetails.fetch(self.course_location)
|
details = CourseDetails.fetch(self.course_location)
|
||||||
@@ -86,7 +87,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
|||||||
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
|
self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
|
||||||
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
|
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
|
||||||
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
|
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
|
||||||
|
|
||||||
def test_encoder(self):
|
def test_encoder(self):
|
||||||
details = CourseDetails.fetch(self.course_location)
|
details = CourseDetails.fetch(self.course_location)
|
||||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||||
@@ -100,7 +101,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
|||||||
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
|
self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
|
||||||
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
|
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
|
||||||
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
|
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
|
||||||
|
|
||||||
def test_update_and_fetch(self):
|
def test_update_and_fetch(self):
|
||||||
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
|
## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions
|
||||||
jsondetails = CourseDetails.fetch(self.course_location)
|
jsondetails = CourseDetails.fetch(self.course_location)
|
||||||
@@ -118,6 +119,7 @@ class CourseDetailsTestCase(CourseTestCase):
|
|||||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||||
jsondetails.effort, "After set effort")
|
jsondetails.effort, "After set effort")
|
||||||
|
|
||||||
|
|
||||||
class CourseDetailsViewTest(CourseTestCase):
|
class CourseDetailsViewTest(CourseTestCase):
|
||||||
def alter_field(self, url, details, field, val):
|
def alter_field(self, url, details, field, val):
|
||||||
setattr(details, field, val)
|
setattr(details, field, val)
|
||||||
@@ -128,36 +130,36 @@ class CourseDetailsViewTest(CourseTestCase):
|
|||||||
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
|
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
|
||||||
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
|
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
|
||||||
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
|
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
|
||||||
resp = self.client.post(url, json.dumps(payload), "application/json")
|
resp = self.client.post(url, json.dumps(payload), "application/json")
|
||||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
|
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_datetime_to_iso(datetime):
|
def convert_datetime_to_iso(datetime):
|
||||||
if datetime is not None:
|
if datetime is not None:
|
||||||
return datetime.isoformat("T")
|
return datetime.isoformat("T")
|
||||||
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,
|
resp = self.client.get(reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||||
'name' : self.course_location.name }))
|
'name': self.course_location.name}))
|
||||||
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
|
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
|
||||||
|
|
||||||
# resp s/b json from here on
|
# resp s/b json from here on
|
||||||
url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
|
url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
|
||||||
'name' : self.course_location.name, 'section' : 'details' })
|
'name': self.course_location.name, 'section': 'details'})
|
||||||
resp = self.client.get(url)
|
resp = self.client.get(url)
|
||||||
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
|
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
|
||||||
|
|
||||||
utc = UTC()
|
utc = UTC()
|
||||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc))
|
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
|
||||||
self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc))
|
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
|
||||||
self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc))
|
self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
|
||||||
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc))
|
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
|
||||||
|
|
||||||
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc))
|
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
|
||||||
self.alter_field(url, details, 'overview', "Overview")
|
self.alter_field(url, details, 'overview', "Overview")
|
||||||
self.alter_field(url, details, 'intro_video', "intro_video")
|
self.alter_field(url, details, 'intro_video', "intro_video")
|
||||||
self.alter_field(url, details, 'effort', "effort")
|
self.alter_field(url, details, 'effort', "effort")
|
||||||
@@ -170,7 +172,7 @@ class CourseDetailsViewTest(CourseTestCase):
|
|||||||
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
|
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
|
||||||
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
|
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
|
||||||
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
|
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
|
||||||
|
|
||||||
def compare_date_fields(self, details, encoded, context, field):
|
def compare_date_fields(self, details, encoded, context, field):
|
||||||
if details[field] is not None:
|
if details[field] is not None:
|
||||||
if field in encoded and encoded[field] is not None:
|
if field in encoded and encoded[field] is not None:
|
||||||
@@ -182,14 +184,15 @@ class CourseDetailsViewTest(CourseTestCase):
|
|||||||
else:
|
else:
|
||||||
details_encoded = jsdate_to_time(details[field])
|
details_encoded = jsdate_to_time(details[field])
|
||||||
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded)
|
||||||
|
|
||||||
expected_delta = datetime.timedelta(0)
|
expected_delta = datetime.timedelta(0)
|
||||||
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
|
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
|
||||||
else:
|
else:
|
||||||
self.fail(field + " missing from encoded but in details at " + context)
|
self.fail(field + " missing from encoded but in details at " + context)
|
||||||
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)
|
||||||
@@ -209,56 +212,56 @@ class CourseGradingTest(CourseTestCase):
|
|||||||
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
self.assertEqual(self.course_location, test_grader.course_location, "Course locations")
|
||||||
self.assertIsNotNone(test_grader.graders, "No graders")
|
self.assertIsNotNone(test_grader.graders, "No graders")
|
||||||
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
|
||||||
|
|
||||||
for i, grader in enumerate(test_grader.graders):
|
for i, grader in enumerate(test_grader.graders):
|
||||||
subgrader = CourseGradingModel.fetch_grader(self.course_location, i)
|
subgrader = CourseGradingModel.fetch_grader(self.course_location, i)
|
||||||
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
|
self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
|
||||||
|
|
||||||
subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0)
|
subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0)
|
||||||
self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
|
self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
|
||||||
|
|
||||||
def test_fetch_cutoffs(self):
|
def test_fetch_cutoffs(self):
|
||||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location)
|
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location)
|
||||||
# ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
|
# ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
|
||||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch")
|
self.assertIsNotNone(test_grader, "No cutoffs via fetch")
|
||||||
|
|
||||||
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url())
|
test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url())
|
||||||
self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
|
self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
|
||||||
|
|
||||||
def test_fetch_grace(self):
|
def test_fetch_grace(self):
|
||||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location)
|
test_grader = CourseGradingModel.fetch_grace_period(self.course_location)
|
||||||
# almost a worthless test
|
# almost a worthless test
|
||||||
self.assertIn('grace_period', test_grader, "No grace via fetch")
|
self.assertIn('grace_period', test_grader, "No grace via fetch")
|
||||||
|
|
||||||
test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url())
|
test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url())
|
||||||
self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
|
self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
|
||||||
|
|
||||||
def test_update_from_json(self):
|
def test_update_from_json(self):
|
||||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
|
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
|
||||||
|
|
||||||
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
|
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
|
||||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
|
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
|
||||||
|
|
||||||
test_grader.grade_cutoffs['D'] = 0.3
|
test_grader.grade_cutoffs['D'] = 0.3
|
||||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||||
|
|
||||||
test_grader.grace_period = {'hours' : '4'}
|
test_grader.grace_period = {'hours' : '4'}
|
||||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||||
|
|
||||||
def test_update_grader_from_json(self):
|
def test_update_grader_from_json(self):
|
||||||
test_grader = CourseGradingModel.fetch(self.course_location)
|
test_grader = CourseGradingModel.fetch(self.course_location)
|
||||||
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, "Noop update")
|
self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
|
||||||
|
|
||||||
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
|
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
|
||||||
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, "min_count[1] + 2")
|
self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")
|
||||||
|
|
||||||
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,29 +2,30 @@ 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")
|
||||||
|
|
||||||
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
|
self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div")
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ from cms.djangoapps.contentstore import utils
|
|||||||
import mock
|
import mock
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class LMSLinksTestCase(TestCase):
|
class LMSLinksTestCase(TestCase):
|
||||||
def about_page_test(self):
|
def about_page_test(self):
|
||||||
location = 'i4x','mitX','101','course', 'test'
|
location = 'i4x', 'mitX', '101', 'course', 'test'
|
||||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||||
link = utils.get_lms_link_for_about_page(location)
|
link = utils.get_lms_link_for_about_page(location)
|
||||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
|
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
|
||||||
|
|
||||||
def ls_link_test(self):
|
def ls_link_test(self):
|
||||||
location = 'i4x','mitX','101','vertical', 'contacting_us'
|
location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us'
|
||||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||||
link = utils.get_lms_link_for_item(location, False)
|
link = utils.get_lms_link_for_item(location, False)
|
||||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from xmodule.seq_module import SequenceDescriptor
|
|||||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||||
from utils import ModuleStoreTestCase, parse_json, user, registration
|
from utils import ModuleStoreTestCase, parse_json, user, registration
|
||||||
|
|
||||||
|
|
||||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||||
def _login(self, email, pw):
|
def _login(self, email, pw):
|
||||||
"""Login. View should always return 200. The success/fail is in the
|
"""Login. View should always return 200. The success/fail is in the
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from django.contrib.auth.models import User
|
|||||||
import xmodule.modulestore.django
|
import xmodule.modulestore.django
|
||||||
from xmodule.templates import update_templates
|
from xmodule.templates import update_templates
|
||||||
|
|
||||||
|
|
||||||
class ModuleStoreTestCase(TestCase):
|
class ModuleStoreTestCase(TestCase):
|
||||||
""" Subclass for any test case that uses the mongodb
|
""" Subclass for any test case that uses the mongodb
|
||||||
module store. This populates a uniquely named modulestore
|
module store. This populates a uniquely named modulestore
|
||||||
collection with templates before running the TestCase
|
collection with templates before running the TestCase
|
||||||
and drops it they are finished. """
|
and drops it they are finished. """
|
||||||
@@ -22,7 +23,7 @@ class ModuleStoreTestCase(TestCase):
|
|||||||
|
|
||||||
# Use the current seconds since epoch to differentiate
|
# Use the current seconds since epoch to differentiate
|
||||||
# the mongo collections on jenkins.
|
# the mongo collections on jenkins.
|
||||||
sec_since_epoch = '%s' % int(time()*100)
|
sec_since_epoch = '%s' % int(time() * 100)
|
||||||
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||||
self.test_MODULESTORE = self.orig_MODULESTORE
|
self.test_MODULESTORE = self.orig_MODULESTORE
|
||||||
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
|
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
|
||||||
@@ -50,14 +51,17 @@ class ModuleStoreTestCase(TestCase):
|
|||||||
|
|
||||||
super(ModuleStoreTestCase, self)._post_teardown()
|
super(ModuleStoreTestCase, self)._post_teardown()
|
||||||
|
|
||||||
|
|
||||||
def parse_json(response):
|
def parse_json(response):
|
||||||
"""Parse response, which is assumed to be json"""
|
"""Parse response, which is assumed to be json"""
|
||||||
return json.loads(response.content)
|
return json.loads(response.content)
|
||||||
|
|
||||||
|
|
||||||
def user(email):
|
def user(email):
|
||||||
"""look up a user by email"""
|
"""look up a user by email"""
|
||||||
return User.objects.get(email=email)
|
return User.objects.get(email=email)
|
||||||
|
|
||||||
|
|
||||||
def registration(email):
|
def registration(email):
|
||||||
"""look up registration object by email"""
|
"""look up registration object by email"""
|
||||||
return Registration.objects.get(user__email=email)
|
return Registration.objects.get(user__email=email)
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ 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
|
||||||
"""
|
"""
|
||||||
if not isinstance(location, Location):
|
if not isinstance(location, Location):
|
||||||
location = Location(location)
|
location = Location(location)
|
||||||
|
|
||||||
if location.category in DIRECT_ONLY_CATEGORIES:
|
if location.category in DIRECT_ONLY_CATEGORIES:
|
||||||
return modulestore('direct')
|
return modulestore('direct')
|
||||||
else:
|
else:
|
||||||
return modulestore()
|
return modulestore()
|
||||||
|
|
||||||
|
|
||||||
def get_course_location_for_item(location):
|
def get_course_location_for_item(location):
|
||||||
'''
|
'''
|
||||||
cdodge: for a given Xmodule, return the course that it belongs to
|
cdodge: for a given Xmodule, return the course that it belongs to
|
||||||
@@ -46,6 +48,7 @@ def get_course_location_for_item(location):
|
|||||||
|
|
||||||
return location
|
return location
|
||||||
|
|
||||||
|
|
||||||
def get_course_for_item(location):
|
def get_course_for_item(location):
|
||||||
'''
|
'''
|
||||||
cdodge: for a given Xmodule, return the course that it belongs to
|
cdodge: for a given Xmodule, return the course that it belongs to
|
||||||
@@ -85,6 +88,7 @@ def get_lms_link_for_item(location, preview=False):
|
|||||||
|
|
||||||
return lms_link
|
return lms_link
|
||||||
|
|
||||||
|
|
||||||
def get_lms_link_for_about_page(location):
|
def get_lms_link_for_about_page(location):
|
||||||
"""
|
"""
|
||||||
Returns the url to the course about page from the location tuple.
|
Returns the url to the course about page from the location tuple.
|
||||||
@@ -99,6 +103,7 @@ def get_lms_link_for_about_page(location):
|
|||||||
|
|
||||||
return lms_link
|
return lms_link
|
||||||
|
|
||||||
|
|
||||||
def get_course_id(location):
|
def get_course_id(location):
|
||||||
"""
|
"""
|
||||||
Returns the course_id from a given the location tuple.
|
Returns the course_id from a given the location tuple.
|
||||||
@@ -106,6 +111,7 @@ def get_course_id(location):
|
|||||||
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
|
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
|
||||||
return modulestore().get_containing_courses(Location(location))[0].id
|
return modulestore().get_containing_courses(Location(location))[0].id
|
||||||
|
|
||||||
|
|
||||||
class UnitState(object):
|
class UnitState(object):
|
||||||
draft = 'draft'
|
draft = 'draft'
|
||||||
private = 'private'
|
private = 'private'
|
||||||
@@ -135,6 +141,7 @@ def compute_unit_state(unit):
|
|||||||
def get_date_display(date):
|
def get_date_display(date):
|
||||||
return date.strftime("%d %B, %Y at %I:%M %p")
|
return date.strftime("%d %B, %Y at %I:%M %p")
|
||||||
|
|
||||||
|
|
||||||
def update_item(location, value):
|
def update_item(location, value):
|
||||||
"""
|
"""
|
||||||
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
|
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
|
||||||
@@ -142,4 +149,4 @@ def update_item(location, value):
|
|||||||
if value is None:
|
if value is None:
|
||||||
get_modulestore(location).delete_item(location)
|
get_modulestore(location).delete_item(location)
|
||||||
else:
|
else:
|
||||||
get_modulestore(location).update_item(location, value)
|
get_modulestore(location).update_item(location, value)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ def signup(request):
|
|||||||
csrf_token = csrf(request)['csrf_token']
|
csrf_token = csrf(request)['csrf_token']
|
||||||
return render_to_response('signup.html', {'csrf': csrf_token})
|
return render_to_response('signup.html', {'csrf': csrf_token})
|
||||||
|
|
||||||
|
|
||||||
@ssl_login_shortcut
|
@ssl_login_shortcut
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def login_page(request):
|
def login_page(request):
|
||||||
@@ -114,7 +115,7 @@ def index(request):
|
|||||||
courses = filter(course_filter, courses)
|
courses = filter(course_filter, courses)
|
||||||
|
|
||||||
return render_to_response('index.html', {
|
return render_to_response('index.html', {
|
||||||
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
||||||
'courses': [(course.metadata.get('display_name'),
|
'courses': [(course.metadata.get('display_name'),
|
||||||
reverse('course_index', args=[
|
reverse('course_index', args=[
|
||||||
course.location.org,
|
course.location.org,
|
||||||
@@ -159,10 +160,10 @@ def course_index(request, org, course, name):
|
|||||||
if not has_access(request.user, location):
|
if not has_access(request.user, location):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
upload_asset_callback_url = reverse('upload_asset', kwargs = {
|
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||||
'org' : org,
|
'org': org,
|
||||||
'course' : course,
|
'course': course,
|
||||||
'coursename' : name
|
'coursename': name
|
||||||
})
|
})
|
||||||
|
|
||||||
course = modulestore().get_item(location)
|
course = modulestore().get_item(location)
|
||||||
@@ -213,7 +214,7 @@ def edit_subsection(request, location):
|
|||||||
|
|
||||||
# remove all metadata from the generic dictionary that is presented in a more normalized UI
|
# remove all metadata from the generic dictionary that is presented in a more normalized UI
|
||||||
|
|
||||||
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
|
policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
|
||||||
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
|
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
|
||||||
|
|
||||||
can_view_live = False
|
can_view_live = False
|
||||||
@@ -233,9 +234,9 @@ def edit_subsection(request, location):
|
|||||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||||
'parent_location': course.location,
|
'parent_location': course.location,
|
||||||
'parent_item': parent,
|
'parent_item': parent,
|
||||||
'policy_metadata' : policy_metadata,
|
'policy_metadata': policy_metadata,
|
||||||
'subsection_units' : subsection_units,
|
'subsection_units': subsection_units,
|
||||||
'can_view_live' : can_view_live
|
'can_view_live': can_view_live
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -294,7 +295,7 @@ def edit_unit(request, location):
|
|||||||
# so let's generate the link url here
|
# so let's generate the link url here
|
||||||
|
|
||||||
# need to figure out where this item is in the list of children as the preview will need this
|
# need to figure out where this item is in the list of children as the preview will need this
|
||||||
index =1
|
index = 1
|
||||||
for child in containing_subsection.get_children():
|
for child in containing_subsection.get_children():
|
||||||
if child.location == item.location:
|
if child.location == item.location:
|
||||||
break
|
break
|
||||||
@@ -348,6 +349,7 @@ def preview_component(request, location):
|
|||||||
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
|
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -362,7 +364,7 @@ def assignment_type_update(request, org, course, category, name):
|
|||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
elif request.method == 'POST': # post or put, doesn't matter.
|
elif request.method == 'POST': # post or put, doesn't matter.
|
||||||
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
|
|
||||||
@@ -527,7 +529,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
|||||||
module.get_html = replace_static_urls(
|
module.get_html = replace_static_urls(
|
||||||
module.get_html,
|
module.get_html,
|
||||||
module.metadata.get('data_dir', module.location.course),
|
module.metadata.get('data_dir', module.location.course),
|
||||||
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])
|
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
|
||||||
)
|
)
|
||||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||||
module.get_instance_state(), module.get_shared_state())
|
module.get_instance_state(), module.get_shared_state())
|
||||||
@@ -588,7 +590,7 @@ def delete_item(request):
|
|||||||
# semantics of delete_item whereby the store is draft aware. Right now calling
|
# semantics of delete_item whereby the store is draft aware. Right now calling
|
||||||
# delete_item on a vertical tries to delete the draft version leaving the
|
# delete_item on a vertical tries to delete the draft version leaving the
|
||||||
# requested delete to never occur
|
# requested delete to never occur
|
||||||
if item.location.revision is None and item.location.category=='vertical' and delete_all_versions:
|
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
|
||||||
modulestore('direct').delete_item(item.location)
|
modulestore('direct').delete_item(item.location)
|
||||||
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
@@ -664,6 +666,7 @@ def create_draft(request):
|
|||||||
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@expect_json
|
@expect_json
|
||||||
def publish_draft(request):
|
def publish_draft(request):
|
||||||
@@ -693,6 +696,7 @@ def unpublish_unit(request):
|
|||||||
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@expect_json
|
@expect_json
|
||||||
def clone_item(request):
|
def clone_item(request):
|
||||||
@@ -725,6 +729,8 @@ def clone_item(request):
|
|||||||
|
|
||||||
#@login_required
|
#@login_required
|
||||||
#@ensure_csrf_cookie
|
#@ensure_csrf_cookie
|
||||||
|
|
||||||
|
|
||||||
def upload_asset(request, org, course, coursename):
|
def upload_asset(request, org, course, coursename):
|
||||||
'''
|
'''
|
||||||
cdodge: this method allows for POST uploading of files into the course asset library, which will
|
cdodge: this method allows for POST uploading of files into the course asset library, which will
|
||||||
@@ -775,11 +781,11 @@ def upload_asset(request, org, course, coursename):
|
|||||||
# readback the saved content - we need the database timestamp
|
# readback the saved content - we need the database timestamp
|
||||||
readback = contentstore().find(content.location)
|
readback = contentstore().find(content.location)
|
||||||
|
|
||||||
response_payload = {'displayname' : content.name,
|
response_payload = {'displayname': content.name,
|
||||||
'uploadDate' : get_date_display(readback.last_modified_at),
|
'uploadDate': get_date_display(readback.last_modified_at),
|
||||||
'url' : StaticContent.get_url_path_from_location(content.location),
|
'url': StaticContent.get_url_path_from_location(content.location),
|
||||||
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||||
'msg' : 'Upload completed'
|
'msg': 'Upload completed'
|
||||||
}
|
}
|
||||||
|
|
||||||
response = HttpResponse(json.dumps(response_payload))
|
response = HttpResponse(json.dumps(response_payload))
|
||||||
@@ -789,6 +795,8 @@ def upload_asset(request, org, course, coursename):
|
|||||||
'''
|
'''
|
||||||
This view will return all CMS users who are editors for the specified course
|
This view will return all CMS users who are editors for the specified course
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def manage_users(request, location):
|
def manage_users(request, location):
|
||||||
@@ -803,16 +811,16 @@ def manage_users(request, location):
|
|||||||
'active_tab': 'users',
|
'active_tab': 'users',
|
||||||
'context_course': course_module,
|
'context_course': course_module,
|
||||||
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
|
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
|
||||||
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
|
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
|
||||||
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/'),
|
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
|
||||||
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
||||||
'request_user_id' : request.user.id
|
'request_user_id': request.user.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def create_json_response(errmsg = None):
|
def create_json_response(errmsg=None):
|
||||||
if errmsg is not None:
|
if errmsg is not None:
|
||||||
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg}))
|
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
|
||||||
else:
|
else:
|
||||||
resp = HttpResponse(json.dumps({'Status': 'OK'}))
|
resp = HttpResponse(json.dumps({'Status': 'OK'}))
|
||||||
|
|
||||||
@@ -822,13 +830,15 @@ def create_json_response(errmsg = None):
|
|||||||
This POST-back view will add a user - specified by email - to the list of editors for
|
This POST-back view will add a user - specified by email - to the list of editors for
|
||||||
the specified course
|
the specified course
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def add_user(request, location):
|
def add_user(request, location):
|
||||||
email = request.POST["email"]
|
email = request.POST["email"]
|
||||||
|
|
||||||
if email=='':
|
if email == '':
|
||||||
return create_json_response('Please specify an email address.')
|
return create_json_response('Please specify an email address.')
|
||||||
|
|
||||||
# check that logged in user has admin permissions to this course
|
# check that logged in user has admin permissions to this course
|
||||||
@@ -854,6 +864,8 @@ def add_user(request, location):
|
|||||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||||
the specified course
|
the specified course
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -881,6 +893,7 @@ def remove_user(request, location):
|
|||||||
def landing(request, org, course, coursename):
|
def landing(request, org, course, coursename):
|
||||||
return render_to_response('temp-course-landing.html', {})
|
return render_to_response('temp-course-landing.html', {})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def static_pages(request, org, course, coursename):
|
def static_pages(request, org, course, coursename):
|
||||||
@@ -921,7 +934,7 @@ def reorder_static_tabs(request):
|
|||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
# load all reference tabs, return BadRequest if we can't find any of them
|
# load all reference tabs, return BadRequest if we can't find any of them
|
||||||
tab_items =[]
|
tab_items = []
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
item = modulestore('direct').get_item(Location(tab))
|
item = modulestore('direct').get_item(Location(tab))
|
||||||
if item is None:
|
if item is None:
|
||||||
@@ -935,8 +948,8 @@ def reorder_static_tabs(request):
|
|||||||
for tab in course.tabs:
|
for tab in course.tabs:
|
||||||
if tab['type'] == 'static_tab':
|
if tab['type'] == 'static_tab':
|
||||||
reordered_tabs.append({'type': 'static_tab',
|
reordered_tabs.append({'type': 'static_tab',
|
||||||
'name' : tab_items[static_tab_idx].metadata.get('display_name'),
|
'name': tab_items[static_tab_idx].metadata.get('display_name'),
|
||||||
'url_slug' : tab_items[static_tab_idx].location.name})
|
'url_slug': tab_items[static_tab_idx].location.name})
|
||||||
static_tab_idx += 1
|
static_tab_idx += 1
|
||||||
else:
|
else:
|
||||||
reordered_tabs.append(tab)
|
reordered_tabs.append(tab)
|
||||||
@@ -980,10 +993,11 @@ def edit_tabs(request, org, course, coursename):
|
|||||||
|
|
||||||
return render_to_response('edit-tabs.html', {
|
return render_to_response('edit-tabs.html', {
|
||||||
'active_tab': 'pages',
|
'active_tab': 'pages',
|
||||||
'context_course':course_item,
|
'context_course': course_item,
|
||||||
'components': components
|
'components': components
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def not_found(request):
|
def not_found(request):
|
||||||
return render_to_response('error.html', {'error': '404'})
|
return render_to_response('error.html', {'error': '404'})
|
||||||
|
|
||||||
@@ -1014,11 +1028,12 @@ def course_info(request, org, course, name, provided_id=None):
|
|||||||
return render_to_response('course_info.html', {
|
return render_to_response('course_info.html', {
|
||||||
'active_tab': 'courseinfo-tab',
|
'active_tab': 'courseinfo-tab',
|
||||||
'context_course': course_module,
|
'context_course': course_module,
|
||||||
'url_base' : "/" + org + "/" + course + "/",
|
'url_base': "/" + org + "/" + course + "/",
|
||||||
'course_updates' : json.dumps(get_course_updates(location)),
|
'course_updates': json.dumps(get_course_updates(location)),
|
||||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -1075,8 +1090,8 @@ def module_info(request, module_location):
|
|||||||
else:
|
else:
|
||||||
real_method = request.method
|
real_method = request.method
|
||||||
|
|
||||||
rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true']
|
rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
|
||||||
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links))
|
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
|
||||||
|
|
||||||
# check that logged in user has permissions to this item
|
# check that logged in user has permissions to this item
|
||||||
if not has_access(request.user, location):
|
if not has_access(request.user, location):
|
||||||
@@ -1089,6 +1104,7 @@ def module_info(request, module_location):
|
|||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def get_course_settings(request, org, course, name):
|
def get_course_settings(request, org, course, name):
|
||||||
@@ -1109,9 +1125,10 @@ def get_course_settings(request, org, course, name):
|
|||||||
return render_to_response('settings.html', {
|
return render_to_response('settings.html', {
|
||||||
'active_tab': 'settings',
|
'active_tab': 'settings',
|
||||||
'context_course': course_module,
|
'context_course': course_module,
|
||||||
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
|
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -1137,12 +1154,13 @@ def course_settings_updates(request, org, course, name, section):
|
|||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
# Cannot just do a get w/o knowing the course name :-(
|
# Cannot just do a get w/o knowing the course name :-(
|
||||||
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
|
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
elif request.method == 'POST': # post or put, doesn't matter.
|
elif request.method == 'POST': # post or put, doesn't matter.
|
||||||
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
@expect_json
|
@expect_json
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -1167,14 +1185,14 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
|||||||
|
|
||||||
if real_method == 'GET':
|
if real_method == 'GET':
|
||||||
# Cannot just do a get w/o knowing the course name :-(
|
# Cannot just do a get w/o knowing the course name :-(
|
||||||
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
|
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
elif real_method == "DELETE":
|
elif real_method == "DELETE":
|
||||||
# ??? Shoudl this return anything? Perhaps success fail?
|
# ??? Shoudl this return anything? Perhaps success fail?
|
||||||
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index)
|
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index)
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
elif request.method == 'POST': # post or put, doesn't matter.
|
elif request.method == 'POST': # post or put, doesn't matter.
|
||||||
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course',name]), request.POST)),
|
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
@@ -1193,10 +1211,10 @@ def asset_index(request, org, course, name):
|
|||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
upload_asset_callback_url = reverse('upload_asset', kwargs = {
|
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||||
'org' : org,
|
'org': org,
|
||||||
'course' : course,
|
'course': course,
|
||||||
'coursename' : name
|
'coursename': name
|
||||||
})
|
})
|
||||||
|
|
||||||
course_module = modulestore().get_item(location)
|
course_module = modulestore().get_item(location)
|
||||||
@@ -1237,13 +1255,14 @@ def asset_index(request, org, course, name):
|
|||||||
def edge(request):
|
def edge(request):
|
||||||
return render_to_response('university_profiles/edge.html', {})
|
return render_to_response('university_profiles/edge.html', {})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@expect_json
|
@expect_json
|
||||||
def create_new_course(request):
|
def create_new_course(request):
|
||||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||||
# so if you change anything here, you need to also change it there.
|
# so if you change anything here, you need to also change it there.
|
||||||
# TODO: write a test that creates two courses, one with the factory and
|
# TODO: write a test that creates two courses, one with the factory and
|
||||||
# the other with this method, then compare them to make sure they are
|
# the other with this method, then compare them to make sure they are
|
||||||
# equivalent.
|
# equivalent.
|
||||||
template = Location(request.POST['template'])
|
template = Location(request.POST['template'])
|
||||||
org = request.POST.get('org')
|
org = request.POST.get('org')
|
||||||
@@ -1288,6 +1307,7 @@ def create_new_course(request):
|
|||||||
|
|
||||||
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
return HttpResponse(json.dumps({'id': new_course.location.url()}))
|
||||||
|
|
||||||
|
|
||||||
def initialize_course_tabs(course):
|
def initialize_course_tabs(course):
|
||||||
# set up the default tabs
|
# set up the default tabs
|
||||||
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
|
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
|
||||||
@@ -1297,7 +1317,7 @@ def initialize_course_tabs(course):
|
|||||||
|
|
||||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||||
# so if you change anything here, you need to also change it there.
|
# so if you change anything here, you need to also change it there.
|
||||||
course.tabs = [{"type": "courseware"},
|
course.tabs = [{"type": "courseware"},
|
||||||
{"type": "course_info", "name": "Course Info"},
|
{"type": "course_info", "name": "Course Info"},
|
||||||
{"type": "discussion", "name": "Discussion"},
|
{"type": "discussion", "name": "Discussion"},
|
||||||
{"type": "wiki", "name": "Wiki"},
|
{"type": "wiki", "name": "Wiki"},
|
||||||
@@ -1305,6 +1325,7 @@ def initialize_course_tabs(course):
|
|||||||
|
|
||||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@login_required
|
@login_required
|
||||||
def import_course(request, org, course, name):
|
def import_course(request, org, course, name):
|
||||||
@@ -1343,7 +1364,7 @@ def import_course(request, org, course, name):
|
|||||||
|
|
||||||
# find the 'course.xml' file
|
# find the 'course.xml' file
|
||||||
|
|
||||||
for r,d,f in os.walk(course_dir):
|
for r, d, f in os.walk(course_dir):
|
||||||
for files in f:
|
for files in f:
|
||||||
if files == 'course.xml':
|
if files == 'course.xml':
|
||||||
break
|
break
|
||||||
@@ -1357,10 +1378,10 @@ def import_course(request, org, course, name):
|
|||||||
|
|
||||||
if r != course_dir:
|
if r != course_dir:
|
||||||
for fname in os.listdir(r):
|
for fname in os.listdir(r):
|
||||||
shutil.move(r/fname, course_dir)
|
shutil.move(r / fname, course_dir)
|
||||||
|
|
||||||
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
||||||
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location))
|
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
|
||||||
|
|
||||||
# we can blow this away when we're done importing.
|
# we can blow this away when we're done importing.
|
||||||
shutil.rmtree(course_dir)
|
shutil.rmtree(course_dir)
|
||||||
@@ -1376,12 +1397,13 @@ def import_course(request, org, course, name):
|
|||||||
return render_to_response('import.html', {
|
return render_to_response('import.html', {
|
||||||
'context_course': course_module,
|
'context_course': course_module,
|
||||||
'active_tab': 'import',
|
'active_tab': 'import',
|
||||||
'successful_import_redirect_url' : reverse('course_index', args=[
|
'successful_import_redirect_url': reverse('course_index', args=[
|
||||||
course_module.location.org,
|
course_module.location.org,
|
||||||
course_module.location.course,
|
course_module.location.course,
|
||||||
course_module.location.name])
|
course_module.location.name])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@login_required
|
@login_required
|
||||||
def generate_export_course(request, org, course, name):
|
def generate_export_course(request, org, course, name):
|
||||||
@@ -1391,7 +1413,7 @@ def generate_export_course(request, org, course, name):
|
|||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
loc = Location(location)
|
loc = Location(location)
|
||||||
export_file = NamedTemporaryFile(prefix=name+'.', suffix=".tar.gz")
|
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
||||||
|
|
||||||
root_dir = path(mkdtemp())
|
root_dir = path(mkdtemp())
|
||||||
|
|
||||||
@@ -1404,11 +1426,11 @@ def generate_export_course(request, org, course, name):
|
|||||||
|
|
||||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||||
tf.add(root_dir/name, arcname=name)
|
tf.add(root_dir / name, arcname=name)
|
||||||
tf.close()
|
tf.close()
|
||||||
|
|
||||||
# remove temp dir
|
# remove temp dir
|
||||||
shutil.rmtree(root_dir/name)
|
shutil.rmtree(root_dir / name)
|
||||||
|
|
||||||
wrapper = FileWrapper(export_file)
|
wrapper = FileWrapper(export_file)
|
||||||
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
||||||
@@ -1430,9 +1452,10 @@ def export_course(request, org, course, name):
|
|||||||
return render_to_response('export.html', {
|
return render_to_response('export.html', {
|
||||||
'context_course': course_module,
|
'context_course': course_module,
|
||||||
'active_tab': 'export',
|
'active_tab': 'export',
|
||||||
'successful_import_redirect_url' : ''
|
'successful_import_redirect_url': ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def event(request):
|
def event(request):
|
||||||
'''
|
'''
|
||||||
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
|
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ class CourseDetails(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
course_location = Location(course_location)
|
course_location = Location(course_location)
|
||||||
|
|
||||||
course = cls(course_location)
|
course = cls(course_location)
|
||||||
|
|
||||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||||
|
|
||||||
course.start_date = descriptor.start
|
course.start_date = descriptor.start
|
||||||
course.end_date = descriptor.end
|
course.end_date = descriptor.end
|
||||||
course.enrollment_start = descriptor.enrollment_start
|
course.enrollment_start = descriptor.enrollment_start
|
||||||
course.enrollment_end = descriptor.enrollment_end
|
course.enrollment_end = descriptor.enrollment_end
|
||||||
|
|
||||||
temploc = course_location._replace(category='about', name='syllabus')
|
temploc = course_location._replace(category='about', name='syllabus')
|
||||||
try:
|
try:
|
||||||
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
|
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||||
@@ -52,32 +52,32 @@ class CourseDetails(object):
|
|||||||
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
|
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
temploc = temploc._replace(name='effort')
|
temploc = temploc._replace(name='effort')
|
||||||
try:
|
try:
|
||||||
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
|
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
temploc = temploc._replace(name='video')
|
temploc = temploc._replace(name='video')
|
||||||
try:
|
try:
|
||||||
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
|
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return course
|
return course
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_from_json(cls, jsondict):
|
def update_from_json(cls, jsondict):
|
||||||
"""
|
"""
|
||||||
Decode the json into CourseDetails and save any changed attrs to the db
|
Decode the json into CourseDetails and save any changed attrs to the db
|
||||||
"""
|
"""
|
||||||
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
|
## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
|
||||||
course_location = jsondict['course_location']
|
course_location = jsondict['course_location']
|
||||||
## Will probably want to cache the inflight courses because every blur generates an update
|
## Will probably want to cache the inflight courses because every blur generates an update
|
||||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||||
|
|
||||||
dirty = False
|
dirty = False
|
||||||
|
|
||||||
if 'start_date' in jsondict:
|
if 'start_date' in jsondict:
|
||||||
@@ -87,7 +87,7 @@ class CourseDetails(object):
|
|||||||
if converted != descriptor.start:
|
if converted != descriptor.start:
|
||||||
dirty = True
|
dirty = True
|
||||||
descriptor.start = converted
|
descriptor.start = converted
|
||||||
|
|
||||||
if 'end_date' in jsondict:
|
if 'end_date' in jsondict:
|
||||||
converted = jsdate_to_time(jsondict['end_date'])
|
converted = jsdate_to_time(jsondict['end_date'])
|
||||||
else:
|
else:
|
||||||
@@ -96,7 +96,7 @@ class CourseDetails(object):
|
|||||||
if converted != descriptor.end:
|
if converted != descriptor.end:
|
||||||
dirty = True
|
dirty = True
|
||||||
descriptor.end = converted
|
descriptor.end = converted
|
||||||
|
|
||||||
if 'enrollment_start' in jsondict:
|
if 'enrollment_start' in jsondict:
|
||||||
converted = jsdate_to_time(jsondict['enrollment_start'])
|
converted = jsdate_to_time(jsondict['enrollment_start'])
|
||||||
else:
|
else:
|
||||||
@@ -105,7 +105,7 @@ class CourseDetails(object):
|
|||||||
if converted != descriptor.enrollment_start:
|
if converted != descriptor.enrollment_start:
|
||||||
dirty = True
|
dirty = True
|
||||||
descriptor.enrollment_start = converted
|
descriptor.enrollment_start = converted
|
||||||
|
|
||||||
if 'enrollment_end' in jsondict:
|
if 'enrollment_end' in jsondict:
|
||||||
converted = jsdate_to_time(jsondict['enrollment_end'])
|
converted = jsdate_to_time(jsondict['enrollment_end'])
|
||||||
else:
|
else:
|
||||||
@@ -114,10 +114,10 @@ class CourseDetails(object):
|
|||||||
if converted != descriptor.enrollment_end:
|
if converted != descriptor.enrollment_end:
|
||||||
dirty = True
|
dirty = True
|
||||||
descriptor.enrollment_end = converted
|
descriptor.enrollment_end = converted
|
||||||
|
|
||||||
if dirty:
|
if dirty:
|
||||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||||
|
|
||||||
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
|
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
|
||||||
# to make faster, could compare against db or could have client send over a list of which fields changed.
|
# to make faster, could compare against db or could have client send over a list of which fields changed.
|
||||||
temploc = Location(course_location)._replace(category='about', name='syllabus')
|
temploc = Location(course_location)._replace(category='about', name='syllabus')
|
||||||
@@ -125,19 +125,19 @@ class CourseDetails(object):
|
|||||||
|
|
||||||
temploc = temploc._replace(name='overview')
|
temploc = temploc._replace(name='overview')
|
||||||
update_item(temploc, jsondict['overview'])
|
update_item(temploc, jsondict['overview'])
|
||||||
|
|
||||||
temploc = temploc._replace(name='effort')
|
temploc = temploc._replace(name='effort')
|
||||||
update_item(temploc, jsondict['effort'])
|
update_item(temploc, jsondict['effort'])
|
||||||
|
|
||||||
temploc = temploc._replace(name='video')
|
temploc = temploc._replace(name='video')
|
||||||
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
|
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
|
||||||
update_item(temploc, recomposed_video_tag)
|
update_item(temploc, recomposed_video_tag)
|
||||||
|
|
||||||
|
|
||||||
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
|
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
|
||||||
# it persisted correctly
|
# it persisted correctly
|
||||||
return CourseDetails.fetch(course_location)
|
return CourseDetails.fetch(course_location)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_video_tag(raw_video):
|
def parse_video_tag(raw_video):
|
||||||
"""
|
"""
|
||||||
@@ -147,17 +147,17 @@ class CourseDetails(object):
|
|||||||
"""
|
"""
|
||||||
if not raw_video:
|
if not raw_video:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
|
keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video)
|
||||||
if keystring_matcher is None:
|
if keystring_matcher is None:
|
||||||
keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
|
keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video)
|
||||||
|
|
||||||
if keystring_matcher:
|
if keystring_matcher:
|
||||||
return keystring_matcher.group(0)
|
return keystring_matcher.group(0)
|
||||||
else:
|
else:
|
||||||
logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
|
logging.warn("ignoring the content because it doesn't not conform to expected pattern: " + raw_video)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def recompose_video_tag(video_key):
|
def recompose_video_tag(video_key):
|
||||||
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
|
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing
|
||||||
@@ -168,7 +168,7 @@ class CourseDetails(object):
|
|||||||
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
|
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
||||||
class CourseSettingsEncoder(json.JSONEncoder):
|
class CourseSettingsEncoder(json.JSONEncoder):
|
||||||
|
|||||||
@@ -6,55 +6,55 @@ from util import converters
|
|||||||
|
|
||||||
class CourseGradingModel(object):
|
class CourseGradingModel(object):
|
||||||
"""
|
"""
|
||||||
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
|
||||||
"""
|
"""
|
||||||
def __init__(self, course_descriptor):
|
def __init__(self, course_descriptor):
|
||||||
self.course_location = course_descriptor.location
|
self.course_location = course_descriptor.location
|
||||||
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
|
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
|
||||||
self.grade_cutoffs = course_descriptor.grade_cutoffs
|
self.grade_cutoffs = course_descriptor.grade_cutoffs
|
||||||
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch(cls, course_location):
|
def fetch(cls, course_location):
|
||||||
"""
|
"""
|
||||||
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
Fetch the course details for the given course from persistence and return a CourseDetails model.
|
||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
|
|
||||||
model = cls(descriptor)
|
model = cls(descriptor)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fetch_grader(course_location, index):
|
def fetch_grader(course_location, index):
|
||||||
"""
|
"""
|
||||||
Fetch the course's nth grader
|
Fetch the course's nth grader
|
||||||
Returns an empty dict if there's no such grader.
|
Returns an empty dict if there's no such grader.
|
||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||||
|
|
||||||
index = int(index)
|
index = int(index)
|
||||||
if len(descriptor.raw_grader) > index:
|
if len(descriptor.raw_grader) > index:
|
||||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||||
|
|
||||||
# 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
|
||||||
def fetch_cutoffs(course_location):
|
def fetch_cutoffs(course_location):
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +62,7 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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 descriptor.grade_cutoffs
|
return descriptor.grade_cutoffs
|
||||||
|
|
||||||
@@ -73,10 +73,10 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@@ -85,32 +85,32 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
course_location = jsondict['course_location']
|
course_location = jsondict['course_location']
|
||||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||||
|
|
||||||
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
|
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
|
||||||
|
|
||||||
descriptor.raw_grader = graders_parsed
|
descriptor.raw_grader = graders_parsed
|
||||||
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
||||||
|
|
||||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||||
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
|
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
|
||||||
|
|
||||||
return CourseGradingModel.fetch(course_location)
|
return CourseGradingModel.fetch(course_location)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_grader_from_json(course_location, grader):
|
def update_grader_from_json(course_location, grader):
|
||||||
"""
|
"""
|
||||||
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
Create or update the grader of the given type (string key) for the given course. Returns the modified
|
||||||
grader which is a full model on the client but not on the server (just a dict)
|
grader which is a full model on the client but not on the server (just a dict)
|
||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
|
||||||
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
# # but that would require not using CourseDescriptor's field directly. Opinions?
|
||||||
|
|
||||||
# parse removes the id; so, grab it before parse
|
# parse removes the id; so, grab it before parse
|
||||||
index = int(grader.get('id', len(descriptor.raw_grader)))
|
index = int(grader.get('id', len(descriptor.raw_grader)))
|
||||||
grader = CourseGradingModel.parse_grader(grader)
|
grader = CourseGradingModel.parse_grader(grader)
|
||||||
|
|
||||||
@@ -118,11 +118,11 @@ class CourseGradingModel(object):
|
|||||||
descriptor.raw_grader[index] = grader
|
descriptor.raw_grader[index] = grader
|
||||||
else:
|
else:
|
||||||
descriptor.raw_grader.append(grader)
|
descriptor.raw_grader.append(grader)
|
||||||
|
|
||||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||||
|
|
||||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_cutoffs_from_json(course_location, cutoffs):
|
def update_cutoffs_from_json(course_location, cutoffs):
|
||||||
"""
|
"""
|
||||||
@@ -131,18 +131,18 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
descriptor.grade_cutoffs = cutoffs
|
descriptor.grade_cutoffs = cutoffs
|
||||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||||
|
|
||||||
return cutoffs
|
return cutoffs
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_grace_period_from_json(course_location, graceperiodjson):
|
def update_grace_period_from_json(course_location, graceperiodjson):
|
||||||
"""
|
"""
|
||||||
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a
|
||||||
grace_period entry in an enclosing dict. It is also safe to call this method with a value of
|
grace_period entry in an enclosing dict. It is also safe to call this method with a value of
|
||||||
None for graceperiodjson.
|
None for graceperiodjson.
|
||||||
"""
|
"""
|
||||||
@@ -160,7 +160,7 @@ class CourseGradingModel(object):
|
|||||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||||
descriptor.metadata['graceperiod'] = grace_rep
|
descriptor.metadata['graceperiod'] = grace_rep
|
||||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_grader(course_location, index):
|
def delete_grader(course_location, index):
|
||||||
"""
|
"""
|
||||||
@@ -168,16 +168,16 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
index = int(index)
|
index = int(index)
|
||||||
if index < len(descriptor.raw_grader):
|
if index < len(descriptor.raw_grader):
|
||||||
del descriptor.raw_grader[index]
|
del descriptor.raw_grader[index]
|
||||||
# force propagation to definition
|
# force propagation to definition
|
||||||
descriptor.raw_grader = descriptor.raw_grader
|
descriptor.raw_grader = descriptor.raw_grader
|
||||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||||
|
|
||||||
# NOTE cannot delete cutoffs. May be useful to reset
|
# NOTE cannot delete cutoffs. May be useful to reset
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_cutoffs(course_location, cutoffs):
|
def delete_cutoffs(course_location, cutoffs):
|
||||||
"""
|
"""
|
||||||
@@ -185,13 +185,13 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
|
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
|
||||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||||
|
|
||||||
return descriptor.grade_cutoffs
|
return descriptor.grade_cutoffs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_grace_period(course_location):
|
def delete_grace_period(course_location):
|
||||||
"""
|
"""
|
||||||
@@ -199,28 +199,28 @@ class CourseGradingModel(object):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(course_location, Location):
|
if not isinstance(course_location, Location):
|
||||||
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)
|
||||||
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
|
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
|
||||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_section_grader_type(location):
|
def get_section_grader_type(location):
|
||||||
if not isinstance(location, Location):
|
if not isinstance(location, Location):
|
||||||
location = Location(location)
|
location = Location(location)
|
||||||
|
|
||||||
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
|
||||||
def update_section_grader_type(location, jsondict):
|
def update_section_grader_type(location, jsondict):
|
||||||
if not isinstance(location, Location):
|
if not isinstance(location, Location):
|
||||||
location = Location(location)
|
location = Location(location)
|
||||||
|
|
||||||
descriptor = get_modulestore(location).get_item(location)
|
descriptor = get_modulestore(location).get_item(location)
|
||||||
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
|
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
|
||||||
descriptor.metadata['format'] = jsondict.get('graderType')
|
descriptor.metadata['format'] = jsondict.get('graderType')
|
||||||
@@ -228,10 +228,10 @@ class CourseGradingModel(object):
|
|||||||
else:
|
else:
|
||||||
if 'format' in descriptor.metadata: del descriptor.metadata['format']
|
if 'format' in descriptor.metadata: del descriptor.metadata['format']
|
||||||
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
|
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
|
||||||
|
|
||||||
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_set_grace_period(descriptor):
|
def convert_set_grace_period(descriptor):
|
||||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||||
@@ -245,13 +245,13 @@ 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
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -260,6 +260,6 @@ class CourseGradingModel(object):
|
|||||||
if grader['weight']:
|
if grader['weight']:
|
||||||
grader['weight'] *= 100
|
grader['weight'] *= 100
|
||||||
if not 'short_label' in grader:
|
if not 'short_label' in grader:
|
||||||
grader['short_label'] = ""
|
grader['short_label'] = ""
|
||||||
|
|
||||||
return grader
|
return grader
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
This config file extends the test environment configuration
|
This config file extends the test environment configuration
|
||||||
so that we can run the lettuce acceptance tests.
|
so that we can run the lettuce acceptance tests.
|
||||||
"""
|
"""
|
||||||
from .test import *
|
from .test import *
|
||||||
@@ -21,14 +21,14 @@ DATA_DIR = COURSES_ROOT
|
|||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Set this up so that rake lms[acceptance] and running the
|
# Set this up so that rake lms[acceptance] and running the
|
||||||
# harvest command both use the same (test) database
|
# harvest command both use the same (test) database
|
||||||
# which they can flush without messing up your dev db
|
# which they can flush without messing up your dev db
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': ENV_ROOT / "db" / "test_mitx.db",
|
'NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||||
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
|
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ MITX_FEATURES = {
|
|||||||
'USE_DJANGO_PIPELINE': True,
|
'USE_DJANGO_PIPELINE': True,
|
||||||
'GITHUB_PUSH': False,
|
'GITHUB_PUSH': False,
|
||||||
'ENABLE_DISCUSSION_SERVICE': False,
|
'ENABLE_DISCUSSION_SERVICE': False,
|
||||||
'AUTH_USE_MIT_CERTIFICATES' : False,
|
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||||
}
|
}
|
||||||
ENABLE_JASMINE = False
|
ENABLE_JASMINE = False
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ PIPELINE_JS = {
|
|||||||
'source_filenames': sorted(
|
'source_filenames': sorted(
|
||||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
|
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
|
||||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
|
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
|
||||||
) + [ 'js/hesitate.js', 'js/base.js'],
|
) + ['js/hesitate.js', 'js/base.js'],
|
||||||
'output_filename': 'js/cms-application.js',
|
'output_filename': 'js/cms-application.js',
|
||||||
},
|
},
|
||||||
'module-js': {
|
'module-js': {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG
|
|||||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||||
logging_env="dev",
|
logging_env="dev",
|
||||||
tracking_filename="tracking.log",
|
tracking_filename="tracking.log",
|
||||||
dev_env = True,
|
dev_env=True,
|
||||||
debug=True)
|
debug=True)
|
||||||
|
|
||||||
modulestore_options = {
|
modulestore_options = {
|
||||||
@@ -41,7 +41,7 @@ CONTENTSTORE = {
|
|||||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'host': 'localhost',
|
'host': 'localhost',
|
||||||
'db' : 'xcontent',
|
'db': 'xcontent',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
|||||||
TEST_ROOT = path('test_root')
|
TEST_ROOT = path('test_root')
|
||||||
|
|
||||||
# Makes the tests run much faster...
|
# Makes the tests run much faster...
|
||||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||||
|
|
||||||
# Want static files in the same dir for running on jenkins.
|
# Want static files in the same dir for running on jenkins.
|
||||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||||
@@ -62,7 +62,7 @@ CONTENTSTORE = {
|
|||||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'host': 'localhost',
|
'host': 'localhost',
|
||||||
'db' : 'xcontent',
|
'db': 'xcontent',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ DATABASES = {
|
|||||||
LMS_BASE = "localhost:8000"
|
LMS_BASE = "localhost:8000"
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
# This is the cache used for most things. Askbot will not work without a
|
# This is the cache used for most things. Askbot will not work without a
|
||||||
# functioning cache -- it relies on caching to load its settings in places.
|
# functioning cache -- it relies on caching to load its settings in places.
|
||||||
# In staging/prod envs, the sessions also live here.
|
# In staging/prod envs, the sessions also live here.
|
||||||
'default': {
|
'default': {
|
||||||
@@ -103,4 +103,4 @@ CACHES = {
|
|||||||
PASSWORD_HASHERS = (
|
PASSWORD_HASHERS = (
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from django.core.management import execute_manager
|
from django.core.management import execute_manager
|
||||||
import imp
|
import imp
|
||||||
try:
|
try:
|
||||||
imp.find_module('settings') # Assumed to be in the same directory.
|
imp.find_module('settings') # Assumed to be in the same directory.
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import sys
|
import sys
|
||||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. "
|
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. "
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ urlpatterns = ('',
|
|||||||
|
|
||||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
|
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
|
||||||
|
|
||||||
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
|
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
|
||||||
name='static_pages'),
|
name='static_pages'),
|
||||||
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
|
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
|
||||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||||
@@ -56,7 +56,7 @@ urlpatterns = ('',
|
|||||||
|
|
||||||
# this is a generic method to return the data/metadata associated with a xmodule
|
# this is a generic method to return the data/metadata associated with a xmodule
|
||||||
url(r'^module_info/(?P<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
|
url(r'^module_info/(?P<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
|
||||||
|
|
||||||
|
|
||||||
# temporary landing page for a course
|
# temporary landing page for a course
|
||||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from django.db import DEFAULT_DB_ALIAS
|
|||||||
from . import app_settings
|
from . import app_settings
|
||||||
from xmodule.contentstore.content import StaticContent
|
from xmodule.contentstore.content import StaticContent
|
||||||
|
|
||||||
|
|
||||||
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||||
"""
|
"""
|
||||||
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
||||||
@@ -108,11 +109,14 @@ def instance_key(model, instance_or_pk):
|
|||||||
getattr(instance_or_pk, 'pk', instance_or_pk),
|
getattr(instance_or_pk, 'pk', instance_or_pk),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_cached_content(content):
|
def set_cached_content(content):
|
||||||
cache.set(str(content.location), content)
|
cache.set(str(content.location), content)
|
||||||
|
|
||||||
|
|
||||||
def get_cached_content(location):
|
def get_cached_content(location):
|
||||||
return cache.get(str(location))
|
return cache.get(str(location))
|
||||||
|
|
||||||
|
|
||||||
def del_cached_content(location):
|
def del_cached_content(location):
|
||||||
cache.delete(str(location))
|
cache.delete(str(location))
|
||||||
|
|||||||
@@ -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
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def is_course_cohorted(course_id):
|
def is_course_cohorted(course_id):
|
||||||
"""
|
"""
|
||||||
Given a course id, return a boolean for whether or not the course is
|
Given a course id, return a boolean for whether or not the course is
|
||||||
@@ -115,6 +116,7 @@ def get_course_cohorts(course_id):
|
|||||||
|
|
||||||
### Helpers for cohort management views
|
### Helpers for cohort management views
|
||||||
|
|
||||||
|
|
||||||
def get_cohort_by_name(course_id, name):
|
def get_cohort_by_name(course_id, name):
|
||||||
"""
|
"""
|
||||||
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
||||||
@@ -124,6 +126,7 @@ def get_cohort_by_name(course_id, name):
|
|||||||
group_type=CourseUserGroup.COHORT,
|
group_type=CourseUserGroup.COHORT,
|
||||||
name=name)
|
name=name)
|
||||||
|
|
||||||
|
|
||||||
def get_cohort_by_id(course_id, cohort_id):
|
def get_cohort_by_id(course_id, cohort_id):
|
||||||
"""
|
"""
|
||||||
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
|
||||||
@@ -133,6 +136,7 @@ def get_cohort_by_id(course_id, cohort_id):
|
|||||||
group_type=CourseUserGroup.COHORT,
|
group_type=CourseUserGroup.COHORT,
|
||||||
id=cohort_id)
|
id=cohort_id)
|
||||||
|
|
||||||
|
|
||||||
def add_cohort(course_id, name):
|
def add_cohort(course_id, name):
|
||||||
"""
|
"""
|
||||||
Add a cohort to a course. Raises ValueError if a cohort of the same name already
|
Add a cohort to a course. Raises ValueError if a cohort of the same name already
|
||||||
@@ -148,12 +152,14 @@ def add_cohort(course_id, name):
|
|||||||
group_type=CourseUserGroup.COHORT,
|
group_type=CourseUserGroup.COHORT,
|
||||||
name=name)
|
name=name)
|
||||||
|
|
||||||
|
|
||||||
class CohortConflict(Exception):
|
class CohortConflict(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when user to be added is already in another cohort in same course.
|
Raised when user to be added is already in another cohort in same course.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def add_user_to_cohort(cohort, username_or_email):
|
def add_user_to_cohort(cohort, username_or_email):
|
||||||
"""
|
"""
|
||||||
Look up the given user, and if successful, add them to the specified cohort.
|
Look up the given user, and if successful, add them to the specified cohort.
|
||||||
@@ -211,4 +217,3 @@ def delete_empty_cohort(course_id, name):
|
|||||||
name, course_id))
|
name, course_id))
|
||||||
|
|
||||||
cohort.delete()
|
cohort.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.db import models
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CourseUserGroup(models.Model):
|
class CourseUserGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
This model represents groups of users in a course. Groups may have different types,
|
This model represents groups of users in a course. Groups may have different types,
|
||||||
@@ -30,5 +31,3 @@ class CourseUserGroup(models.Model):
|
|||||||
COHORT = 'cohort'
|
COHORT = 'cohort'
|
||||||
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
|
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
|
||||||
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
|
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from xmodule.modulestore.django import modulestore, _MODULESTORES
|
|||||||
|
|
||||||
# NOTE: running this with the lms.envs.test config works without
|
# NOTE: running this with the lms.envs.test config works without
|
||||||
# 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 {
|
||||||
@@ -28,6 +29,7 @@ def xml_store_config(data_dir):
|
|||||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||||
class TestCohorts(django.test.TestCase):
|
class TestCohorts(django.test.TestCase):
|
||||||
|
|
||||||
@@ -184,6 +186,3 @@ class TestCohorts(django.test.TestCase):
|
|||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
is_commentable_cohorted(course.id, to_id("Feedback")),
|
is_commentable_cohorted(course.id, to_id("Feedback")),
|
||||||
"Feedback was listed as cohorted. Should be.")
|
"Feedback was listed as cohorted. Should be.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import track.views
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def json_http_response(data):
|
def json_http_response(data):
|
||||||
"""
|
"""
|
||||||
Return an HttpResponse with the data json-serialized and the right content
|
Return an HttpResponse with the data json-serialized and the right content
|
||||||
@@ -29,6 +30,7 @@ def json_http_response(data):
|
|||||||
"""
|
"""
|
||||||
return HttpResponse(json.dumps(data), content_type="application/json")
|
return HttpResponse(json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
def split_by_comma_and_whitespace(s):
|
def split_by_comma_and_whitespace(s):
|
||||||
"""
|
"""
|
||||||
Split a string both by commas and whitespice. Returns a list.
|
Split a string both by commas and whitespice. Returns a list.
|
||||||
@@ -177,6 +179,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
|
|||||||
'conflict': conflict,
|
'conflict': conflict,
|
||||||
'unknown': unknown})
|
'unknown': unknown})
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@require_POST
|
@require_POST
|
||||||
def remove_user_from_cohort(request, course_id, cohort_id):
|
def remove_user_from_cohort(request, course_id, cohort_id):
|
||||||
|
|||||||
@@ -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,20 +12,20 @@ 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'), )
|
||||||
external_id = models.CharField(max_length=255, db_index=True)
|
external_id = models.CharField(max_length=255, db_index=True)
|
||||||
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,9 +13,10 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
@@ -42,7 +43,7 @@ class MyFetcher(HTTPFetcher):
|
|||||||
if headers and 'Accept' in headers:
|
if headers and 'Accept' in headers:
|
||||||
data['CONTENT_TYPE'] = headers['Accept']
|
data['CONTENT_TYPE'] = headers['Accept']
|
||||||
response = self.client.get(url, data)
|
response = self.client.get(url, data)
|
||||||
|
|
||||||
# Translate the test client response to the fetcher's HTTP response abstraction
|
# Translate the test client response to the fetcher's HTTP response abstraction
|
||||||
content = response.content
|
content = response.content
|
||||||
final_url = url
|
final_url = url
|
||||||
@@ -60,6 +61,7 @@ class MyFetcher(HTTPFetcher):
|
|||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OpenIdProviderTest(TestCase):
|
class OpenIdProviderTest(TestCase):
|
||||||
|
|
||||||
# def setUp(self):
|
# def setUp(self):
|
||||||
@@ -78,7 +80,7 @@ class OpenIdProviderTest(TestCase):
|
|||||||
provider_url = reverse('openid-provider-xrds')
|
provider_url = reverse('openid-provider-xrds')
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
request = factory.request()
|
request = factory.request()
|
||||||
abs_provider_url = request.build_absolute_uri(location = provider_url)
|
abs_provider_url = request.build_absolute_uri(location=provider_url)
|
||||||
|
|
||||||
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
||||||
# in the test environment, we either need a live server that works with the default
|
# in the test environment, we either need a live server that works with the default
|
||||||
@@ -86,10 +88,10 @@ class OpenIdProviderTest(TestCase):
|
|||||||
# Here we do the latter:
|
# Here we do the latter:
|
||||||
fetcher = MyFetcher(self.client)
|
fetcher = MyFetcher(self.client)
|
||||||
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
||||||
|
|
||||||
# now we can begin the login process by invoking a local openid client,
|
# now we can begin the login process by invoking a local openid client,
|
||||||
# with a pointer to the (also-local) openid provider:
|
# with a pointer to the (also-local) openid provider:
|
||||||
with self.settings(OPENID_SSO_SERVER_URL = abs_provider_url):
|
with self.settings(OPENID_SSO_SERVER_URL=abs_provider_url):
|
||||||
url = reverse('openid-login')
|
url = reverse('openid-login')
|
||||||
resp = self.client.post(url)
|
resp = self.client.post(url)
|
||||||
code = 200
|
code = 200
|
||||||
@@ -107,7 +109,7 @@ class OpenIdProviderTest(TestCase):
|
|||||||
provider_url = reverse('openid-provider-login')
|
provider_url = reverse('openid-provider-login')
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
request = factory.request()
|
request = factory.request()
|
||||||
abs_provider_url = request.build_absolute_uri(location = provider_url)
|
abs_provider_url = request.build_absolute_uri(location=provider_url)
|
||||||
|
|
||||||
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
# In order for this absolute URL to work (i.e. to get xrds, then authentication)
|
||||||
# in the test environment, we either need a live server that works with the default
|
# in the test environment, we either need a live server that works with the default
|
||||||
@@ -115,10 +117,10 @@ class OpenIdProviderTest(TestCase):
|
|||||||
# Here we do the latter:
|
# Here we do the latter:
|
||||||
fetcher = MyFetcher(self.client)
|
fetcher = MyFetcher(self.client)
|
||||||
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
openid.fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -143,43 +145,43 @@ class OpenIdProviderTest(TestCase):
|
|||||||
self.assertContains(resp, '<input type="submit" value="Continue" />', html=True)
|
self.assertContains(resp, '<input type="submit" value="Continue" />', html=True)
|
||||||
# this should work on the server:
|
# this should work on the server:
|
||||||
self.assertContains(resp, '<input name="openid.realm" type="hidden" value="http://testserver/" />', html=True)
|
self.assertContains(resp, '<input name="openid.realm" type="hidden" value="http://testserver/" />', html=True)
|
||||||
|
|
||||||
# not included here are elements that will vary from run to run:
|
# not included here are elements that will vary from run to run:
|
||||||
# <input name="openid.return_to" type="hidden" value="http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H" />
|
# <input name="openid.return_to" type="hidden" value="http://testserver/openid/complete/?janrain_nonce=2013-01-23T06%3A20%3A17ZaN7j6H" />
|
||||||
# <input name="openid.assoc_handle" type="hidden" value="{HMAC-SHA1}{50ff8120}{rh87+Q==}" />
|
# <input name="openid.assoc_handle" type="hidden" value="{HMAC-SHA1}{50ff8120}{rh87+Q==}" />
|
||||||
|
|
||||||
|
|
||||||
def testOpenIdSetup(self):
|
def testOpenIdSetup(self):
|
||||||
if not settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
|
if not settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
|
||||||
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
|
||||||
self.assertEqual(resp.status_code, code,
|
self.assertEqual(resp.status_code, code,
|
||||||
"got code {0} for url '{1}'. Expected code {2}"
|
"got code {0} for url '{1}'. Expected code {2}"
|
||||||
.format(resp.status_code, url, code))
|
.format(resp.status_code, url, code))
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
# fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
|
# fetcher (i.e. urlopen2), or a test server that is reached through a custom fetcher.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ def ssl_dn_extract_info(dn):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return (user, email, fullname)
|
return (user, email, fullname)
|
||||||
|
|
||||||
|
|
||||||
def ssl_get_cert_from_request(request):
|
def ssl_get_cert_from_request(request):
|
||||||
"""
|
"""
|
||||||
@@ -460,7 +460,7 @@ def provider_login(request):
|
|||||||
openid_request.answer(False), {})
|
openid_request.answer(False), {})
|
||||||
|
|
||||||
# checkid_setup, so display login page
|
# checkid_setup, so display login page
|
||||||
# (by falling through to the provider_login at the
|
# (by falling through to the provider_login at the
|
||||||
# bottom of this method).
|
# bottom of this method).
|
||||||
elif openid_request.mode == 'checkid_setup':
|
elif openid_request.mode == 'checkid_setup':
|
||||||
if openid_request.idSelect():
|
if openid_request.idSelect():
|
||||||
@@ -482,7 +482,7 @@ def provider_login(request):
|
|||||||
|
|
||||||
# handle login redirection: these are also sent to this view function,
|
# handle login redirection: these are also sent to this view function,
|
||||||
# but are distinguished by lacking the openid mode. We also know that
|
# but are distinguished by lacking the openid mode. We also know that
|
||||||
# they are posts, because they come from the popup
|
# they are posts, because they come from the popup
|
||||||
elif request.method == 'POST' and 'openid_setup' in request.session:
|
elif request.method == 'POST' and 'openid_setup' in request.session:
|
||||||
# get OpenID request from session
|
# get OpenID request from session
|
||||||
openid_setup = request.session['openid_setup']
|
openid_setup = request.session['openid_setup']
|
||||||
@@ -495,7 +495,7 @@ def provider_login(request):
|
|||||||
return default_render_failure(request, "Invalid OpenID trust root")
|
return default_render_failure(request, "Invalid OpenID trust root")
|
||||||
|
|
||||||
# check if user with given email exists
|
# check if user with given email exists
|
||||||
# Failure is redirected to this method (by using the original URL),
|
# Failure is redirected to this method (by using the original URL),
|
||||||
# which will bring up the login dialog.
|
# which will bring up the login dialog.
|
||||||
email = request.POST.get('email', None)
|
email = request.POST.get('email', None)
|
||||||
try:
|
try:
|
||||||
@@ -542,17 +542,17 @@ def provider_login(request):
|
|||||||
# missing fields is up to the Consumer. The proper change
|
# missing fields is up to the Consumer. The proper change
|
||||||
# should only return the username, however this will likely
|
# should only return the username, however this will likely
|
||||||
# break the CS50 client. Temporarily we will be returning
|
# break the CS50 client. Temporarily we will be returning
|
||||||
# username filling in for fullname in addition to username
|
# username filling in for fullname in addition to username
|
||||||
# as sreg nickname.
|
# as sreg nickname.
|
||||||
|
|
||||||
# Note too that this is hardcoded, and not really responding to
|
# Note too that this is hardcoded, and not really responding to
|
||||||
# the extensions that were registered in the first place.
|
# the extensions that were registered in the first place.
|
||||||
results = {
|
results = {
|
||||||
'nickname': user.username,
|
'nickname': user.username,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'fullname': user.username
|
'fullname': user.username
|
||||||
}
|
}
|
||||||
|
|
||||||
# the request succeeded:
|
# the request succeeded:
|
||||||
return provider_respond(server, openid_request, response, results)
|
return provider_respond(server, openid_request, response, results)
|
||||||
|
|
||||||
|
|||||||
@@ -12,34 +12,35 @@ 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
|
||||||
Mako template if the first line is "## mako". It is based off BaseLoader
|
Mako template if the first line is "## mako". It is based off BaseLoader
|
||||||
in django.template.loader.
|
in django.template.loader.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_usable = False
|
is_usable = False
|
||||||
|
|
||||||
def __init__(self, base_loader):
|
def __init__(self, base_loader):
|
||||||
# base_loader is an instance of a BaseLoader subclass
|
# base_loader is an instance of a BaseLoader subclass
|
||||||
self.base_loader = base_loader
|
self.base_loader = base_loader
|
||||||
|
|
||||||
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
|
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
|
||||||
|
|
||||||
if module_directory is None:
|
if module_directory is None:
|
||||||
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
|
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
|
||||||
module_directory = tempfile.mkdtemp()
|
module_directory = tempfile.mkdtemp()
|
||||||
|
|
||||||
self.module_directory = module_directory
|
self.module_directory = module_directory
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, template_name, template_dirs=None):
|
def __call__(self, template_name, template_dirs=None):
|
||||||
return self.load_template(template_name, template_dirs)
|
return self.load_template(template_name, template_dirs)
|
||||||
|
|
||||||
def load_template(self, template_name, template_dirs=None):
|
def load_template(self, template_name, template_dirs=None):
|
||||||
source, file_path = self.load_template_source(template_name, template_dirs)
|
source, file_path = self.load_template_source(template_name, template_dirs)
|
||||||
|
|
||||||
if source.startswith("## mako\n"):
|
if source.startswith("## mako\n"):
|
||||||
# This is a mako template
|
# This is a mako template
|
||||||
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name)
|
template = Template(filename=file_path, module_directory=self.module_directory, uri=template_name)
|
||||||
@@ -56,23 +57,24 @@ class MakoLoader(object):
|
|||||||
# This allows for correct identification (later) of the actual template that does
|
# This allows for correct identification (later) of the actual template that does
|
||||||
# not exist.
|
# not exist.
|
||||||
return source, file_path
|
return source, file_path
|
||||||
|
|
||||||
def load_template_source(self, template_name, template_dirs=None):
|
def load_template_source(self, template_name, template_dirs=None):
|
||||||
# Just having this makes the template load as an instance, instead of a class.
|
# Just having this makes the template load as an instance, instead of a class.
|
||||||
return self.base_loader.load_template_source(template_name, template_dirs)
|
return self.base_loader.load_template_source(template_name, template_dirs)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.base_loader.reset()
|
self.base_loader.reset()
|
||||||
|
|
||||||
|
|
||||||
class MakoFilesystemLoader(MakoLoader):
|
class MakoFilesystemLoader(MakoLoader):
|
||||||
is_usable = True
|
is_usable = True
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
MakoLoader.__init__(self, AppDirectoriesLoader())
|
MakoLoader.__init__(self, AppDirectoriesLoader())
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ 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
|
||||||
be rendered like it is a django template because the arguments are transformed
|
be rendered like it is a django template because the arguments are transformed
|
||||||
in a way that MakoTemplate can understand.
|
in a way that MakoTemplate can understand.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Overrides base __init__ to provide django variable overrides"""
|
"""Overrides base __init__ to provide django variable overrides"""
|
||||||
if not kwargs.get('no_django', False):
|
if not kwargs.get('no_django', False):
|
||||||
@@ -34,8 +36,8 @@ class Template(MakoTemplate):
|
|||||||
overrides['lookup'] = overrides['lookup']['main']
|
overrides['lookup'] = overrides['lookup']['main']
|
||||||
kwargs.update(overrides)
|
kwargs.update(overrides)
|
||||||
super(Template, self).__init__(*args, **kwargs)
|
super(Template, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def render(self, context_instance):
|
def render(self, context_instance):
|
||||||
"""
|
"""
|
||||||
This takes a render call with a context (from Django) and translates
|
This takes a render call with a context (from Django) and translates
|
||||||
@@ -43,7 +45,7 @@ class Template(MakoTemplate):
|
|||||||
"""
|
"""
|
||||||
# collapse context_instance to a single dictionary for mako
|
# collapse context_instance to a single dictionary for mako
|
||||||
context_dictionary = {}
|
context_dictionary = {}
|
||||||
|
|
||||||
# In various testing contexts, there might not be a current request context.
|
# In various testing contexts, there might not be a current request context.
|
||||||
if middleware.requestcontext is not None:
|
if middleware.requestcontext is not None:
|
||||||
for d in middleware.requestcontext:
|
for d in middleware.requestcontext:
|
||||||
@@ -53,5 +55,5 @@ class Template(MakoTemplate):
|
|||||||
context_dictionary['settings'] = settings
|
context_dictionary['settings'] = settings
|
||||||
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
|
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
|
||||||
context_dictionary['django_context'] = context_instance
|
context_dictionary['django_context'] = context_instance
|
||||||
|
|
||||||
return super(Template, self).render_unicode(**context_dictionary)
|
return super(Template, self).render_unicode(**context_dictionary)
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ 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
|
||||||
in the way that a django-style {% include %} does. Pass it context
|
in the way that a django-style {% include %} does. Pass it 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
|||||||
This allows a mako template to call a template tag function (written
|
This allows a mako template to call a template tag function (written
|
||||||
for django templates) that is an "inclusion tag". These functions are
|
for django templates) that is an "inclusion tag". These functions are
|
||||||
decorated with @register.inclusion_tag.
|
decorated with @register.inclusion_tag.
|
||||||
|
|
||||||
-func: This is the function that is registered as an inclusion tag.
|
-func: This is the function that is registered as an inclusion tag.
|
||||||
You must import it directly using a python import statement.
|
You must import it directly using a python import statement.
|
||||||
-file_name: This is the filename of the template, passed into the
|
-file_name: This is the filename of the template, passed into the
|
||||||
@@ -29,10 +30,10 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
|||||||
a copy of the django context is available as 'django_context'.
|
a copy of the django context is available as 'django_context'.
|
||||||
-*args and **kwargs are the arguments to func.
|
-*args and **kwargs are the arguments to func.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if takes_context:
|
if takes_context:
|
||||||
args = [django_context] + list(args)
|
args = [django_context] + list(args)
|
||||||
|
|
||||||
_dict = func(*args, **kwargs)
|
_dict = func(*args, **kwargs)
|
||||||
if isinstance(file_name, Template):
|
if isinstance(file_name, Template):
|
||||||
t = file_name
|
t = file_name
|
||||||
@@ -40,14 +41,12 @@ def render_inclusion(func, file_name, takes_context, django_context, *args, **kw
|
|||||||
t = select_template(file_name)
|
t = select_template(file_name)
|
||||||
else:
|
else:
|
||||||
t = get_template(file_name)
|
t = get_template(file_name)
|
||||||
|
|
||||||
nodelist = t.nodelist
|
nodelist = t.nodelist
|
||||||
|
|
||||||
new_context = Context(_dict)
|
new_context = Context(_dict)
|
||||||
csrf_token = django_context.get('csrf_token', None)
|
csrf_token = django_context.get('csrf_token', None)
|
||||||
if csrf_token is not None:
|
if csrf_token is not None:
|
||||||
new_context['csrf_token'] = csrf_token
|
new_context['csrf_token'] = csrf_token
|
||||||
|
|
||||||
return nodelist.render(new_context)
|
|
||||||
|
|
||||||
|
|
||||||
|
return nodelist.render(new_context)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ def _url_replace_regex(prefix):
|
|||||||
(?P=quote) # the first matching closing quote
|
(?P=quote) # the first matching closing quote
|
||||||
""".format(prefix=prefix)
|
""".format(prefix=prefix)
|
||||||
|
|
||||||
|
|
||||||
def try_staticfiles_lookup(path):
|
def try_staticfiles_lookup(path):
|
||||||
"""
|
"""
|
||||||
Try to lookup a path in staticfiles_storage. If it fails, return
|
Try to lookup a path in staticfiles_storage. If it fails, return
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def test_mongo_filestore(mock_modulestore, mock_static_content):
|
|||||||
|
|
||||||
mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE)
|
mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
@patch('static_replace.settings')
|
@patch('static_replace.settings')
|
||||||
@patch('static_replace.modulestore')
|
@patch('static_replace.modulestore')
|
||||||
@patch('static_replace.staticfiles_storage')
|
@patch('static_replace.staticfiles_storage')
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|||||||
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
|
|||||||
|
|
||||||
from student.views import _do_create_account, get_random_post_override
|
from student.views import _do_create_account, get_random_post_override
|
||||||
|
|
||||||
|
|
||||||
def create(n, course_id):
|
def create(n, course_id):
|
||||||
"""Create n users, enrolling them in course_id if it's not None"""
|
"""Create n users, enrolling them in course_id if it's not None"""
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
@@ -15,6 +16,7 @@ def create(n, course_id):
|
|||||||
if course_id is not None:
|
if course_id is not None:
|
||||||
CourseEnrollment.objects.create(user=user, course_id=course_id)
|
CourseEnrollment.objects.create(user=user, course_id=course_id)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = """Create N new users, with random parameters.
|
help = """Create N new users, with random parameters.
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Command(BaseCommand):
|
|||||||
outputfile = datetime.utcnow().strftime("pearson-dump-%Y%m%d-%H%M%S.json")
|
outputfile = datetime.utcnow().strftime("pearson-dump-%Y%m%d-%H%M%S.json")
|
||||||
else:
|
else:
|
||||||
outputfile = args[0]
|
outputfile = args[0]
|
||||||
|
|
||||||
# construct the query object to dump:
|
# construct the query object to dump:
|
||||||
registrations = TestCenterRegistration.objects.all()
|
registrations = TestCenterRegistration.objects.all()
|
||||||
if 'course_id' in options and options['course_id']:
|
if 'course_id' in options and options['course_id']:
|
||||||
@@ -44,24 +44,24 @@ class Command(BaseCommand):
|
|||||||
if 'exam_series_code' in options and options['exam_series_code']:
|
if 'exam_series_code' in options and options['exam_series_code']:
|
||||||
registrations = registrations.filter(exam_series_code=options['exam_series_code'])
|
registrations = registrations.filter(exam_series_code=options['exam_series_code'])
|
||||||
|
|
||||||
# collect output:
|
# collect output:
|
||||||
output = []
|
output = []
|
||||||
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
|
||||||
@@ -71,8 +71,7 @@ class Command(BaseCommand):
|
|||||||
record['needs_uploading'] = True
|
record['needs_uploading'] = True
|
||||||
|
|
||||||
output.append(record)
|
output.append(record)
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Command(BaseCommand):
|
|||||||
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
|
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
|
||||||
])
|
])
|
||||||
|
|
||||||
# define defaults, even thought 'store_true' shouldn't need them.
|
# define defaults, even thought 'store_true' shouldn't need them.
|
||||||
# (call_command will set None as default value for all options that don't have one,
|
# (call_command will set None as default value for all options that don't have one,
|
||||||
# so one cannot rely on presence/absence of flags in that world.)
|
# so one cannot rely on presence/absence of flags in that world.)
|
||||||
option_list = BaseCommand.option_list + (
|
option_list = BaseCommand.option_list + (
|
||||||
@@ -56,7 +56,7 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
# update time should use UTC in order to be comparable to the user_updated_at
|
# update time should use UTC in order to be comparable to the user_updated_at
|
||||||
# field
|
# field
|
||||||
uploaded_at = datetime.utcnow()
|
uploaded_at = datetime.utcnow()
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class Command(BaseCommand):
|
|||||||
extrasaction='ignore')
|
extrasaction='ignore')
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for tcu in TestCenterUser.objects.order_by('id'):
|
for tcu in TestCenterUser.objects.order_by('id'):
|
||||||
if tcu.needs_uploading: # or dump_all
|
if tcu.needs_uploading: # or dump_all
|
||||||
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
|
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
|
||||||
for csv_field, model_field
|
for csv_field, model_field
|
||||||
in Command.CSV_TO_MODEL_FIELDS.items())
|
in Command.CSV_TO_MODEL_FIELDS.items())
|
||||||
|
|||||||
@@ -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
|
|||||||
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:
|
||||||
@@ -16,23 +17,23 @@ class Command(BaseCommand):
|
|||||||
'--accommodation_request',
|
'--accommodation_request',
|
||||||
action='store',
|
action='store',
|
||||||
dest='accommodation_request',
|
dest='accommodation_request',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--accommodation_code',
|
'--accommodation_code',
|
||||||
action='store',
|
action='store',
|
||||||
dest='accommodation_code',
|
dest='accommodation_code',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--client_authorization_id',
|
'--client_authorization_id',
|
||||||
action='store',
|
action='store',
|
||||||
dest='client_authorization_id',
|
dest='client_authorization_id',
|
||||||
),
|
),
|
||||||
# exam info:
|
# exam info:
|
||||||
make_option(
|
make_option(
|
||||||
'--exam_series_code',
|
'--exam_series_code',
|
||||||
action='store',
|
action='store',
|
||||||
dest='exam_series_code',
|
dest='exam_series_code',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--eligibility_appointment_date_first',
|
'--eligibility_appointment_date_first',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -51,32 +52,32 @@ class Command(BaseCommand):
|
|||||||
action='store',
|
action='store',
|
||||||
dest='authorization_id',
|
dest='authorization_id',
|
||||||
help='ID we receive from Pearson for a particular authorization'
|
help='ID we receive from Pearson for a particular authorization'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--upload_status',
|
'--upload_status',
|
||||||
action='store',
|
action='store',
|
||||||
dest='upload_status',
|
dest='upload_status',
|
||||||
help='status value assigned by Pearson'
|
help='status value assigned by Pearson'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--upload_error_message',
|
'--upload_error_message',
|
||||||
action='store',
|
action='store',
|
||||||
dest='upload_error_message',
|
dest='upload_error_message',
|
||||||
help='error message provided by Pearson on a failure.'
|
help='error message provided by Pearson on a failure.'
|
||||||
),
|
),
|
||||||
# control values:
|
# control values:
|
||||||
make_option(
|
make_option(
|
||||||
'--ignore_registration_dates',
|
'--ignore_registration_dates',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='ignore_registration_dates',
|
dest='ignore_registration_dates',
|
||||||
help='find exam info for course based on exam_series_code, even if the exam is not active.'
|
help='find exam info for course based on exam_series_code, even if the exam is not active.'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--create_dummy_exam',
|
'--create_dummy_exam',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='create_dummy_exam',
|
dest='create_dummy_exam',
|
||||||
help='create dummy exam info for course, even if course exists'
|
help='create dummy exam info for course, even if course exists'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
args = "<student_username course_id>"
|
args = "<student_username course_id>"
|
||||||
help = "Create or modify a TestCenterRegistration entry for a given Student"
|
help = "Create or modify a TestCenterRegistration entry for a given Student"
|
||||||
@@ -103,7 +104,7 @@ class Command(BaseCommand):
|
|||||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||||
except TestCenterUser.DoesNotExist:
|
except TestCenterUser.DoesNotExist:
|
||||||
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
|
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
|
||||||
|
|
||||||
# get an "exam" object. Check to see if a course_id was specified, and use information from that:
|
# get an "exam" object. Check to see if a course_id was specified, and use information from that:
|
||||||
exam = None
|
exam = None
|
||||||
create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
|
create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
|
||||||
@@ -115,14 +116,14 @@ class Command(BaseCommand):
|
|||||||
exam = examlist[0] if len(examlist) > 0 else None
|
exam = examlist[0] if len(examlist) > 0 else None
|
||||||
else:
|
else:
|
||||||
exam = course.current_test_center_exam
|
exam = course.current_test_center_exam
|
||||||
except ItemNotFoundError:
|
except ItemNotFoundError:
|
||||||
pass
|
pass
|
||||||
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
|
||||||
@@ -134,15 +135,15 @@ class Command(BaseCommand):
|
|||||||
raise CommandError("Exam for course_id {} does not exist".format(course_id))
|
raise CommandError("Exam for course_id {} does not exist".format(course_id))
|
||||||
|
|
||||||
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',
|
||||||
'eligibility_appointment_date_first',
|
'eligibility_appointment_date_first',
|
||||||
'eligibility_appointment_date_last',
|
'eligibility_appointment_date_last',
|
||||||
)
|
)
|
||||||
|
|
||||||
# create and save the registration:
|
# create and save the registration:
|
||||||
needs_updating = False
|
needs_updating = False
|
||||||
registrations = get_testcenter_registration(student, course_id, exam_code)
|
registrations = get_testcenter_registration(student, course_id, exam_code)
|
||||||
@@ -152,29 +153,29 @@ 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
|
||||||
|
|
||||||
|
|
||||||
if needs_updating:
|
if needs_updating:
|
||||||
# first update the record with the new values, if any:
|
# first update the record with the new values, if any:
|
||||||
for fieldname in UPDATE_FIELDS:
|
for fieldname in UPDATE_FIELDS:
|
||||||
if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields:
|
if fieldname in our_options and fieldname not in TestCenterRegistrationForm.Meta.fields:
|
||||||
registration.__setattr__(fieldname, our_options[fieldname])
|
registration.__setattr__(fieldname, our_options[fieldname])
|
||||||
|
|
||||||
# the registration form normally populates the data dict with
|
# the registration form normally populates the data dict with
|
||||||
# the accommodation request (if any). But here we want to
|
# the accommodation request (if any). But here we want to
|
||||||
# specify only those values that might change, so update the dict with existing
|
# specify only those values that might change, so update the dict with existing
|
||||||
# values.
|
# values.
|
||||||
form_options = dict(our_options)
|
form_options = dict(our_options)
|
||||||
for propname in TestCenterRegistrationForm.Meta.fields:
|
for propname in TestCenterRegistrationForm.Meta.fields:
|
||||||
if propname not in form_options:
|
if propname not in form_options:
|
||||||
form_options[propname] = registration.__getattribute__(propname)
|
form_options[propname] = registration.__getattribute__(propname)
|
||||||
form = TestCenterRegistrationForm(instance=registration, data=form_options)
|
form = TestCenterRegistrationForm(instance=registration, data=form_options)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.update_and_save()
|
form.update_and_save()
|
||||||
print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format(student.username, course_id, exam_code)
|
print "Updated registration information for user's registration: username \"{}\" course \"{}\", examcode \"{}\"".format(student.username, course_id, exam_code)
|
||||||
else:
|
else:
|
||||||
if (len(form.errors) > 0):
|
if (len(form.errors) > 0):
|
||||||
print "Field Form errors encountered:"
|
print "Field Form errors encountered:"
|
||||||
@@ -185,24 +186,22 @@ class Command(BaseCommand):
|
|||||||
print "Non-field Form errors encountered:"
|
print "Non-field Form errors encountered:"
|
||||||
for nonfielderror in form.non_field_errors:
|
for nonfielderror in form.non_field_errors:
|
||||||
print "Non-field Form Error: %s" % nonfielderror
|
print "Non-field Form Error: %s" % nonfielderror
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print "No changes necessary to make to existing user's registration."
|
print "No changes necessary to make to existing user's registration."
|
||||||
|
|
||||||
# override internal values:
|
# override internal values:
|
||||||
change_internal = False
|
change_internal = False
|
||||||
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
|
||||||
|
|
||||||
if change_internal:
|
if change_internal:
|
||||||
print "Updated confirmation information in existing user's registration."
|
print "Updated confirmation information in existing user's registration."
|
||||||
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,60 +5,61 @@ 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:
|
||||||
make_option(
|
make_option(
|
||||||
'--first_name',
|
'--first_name',
|
||||||
action='store',
|
action='store',
|
||||||
dest='first_name',
|
dest='first_name',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--middle_name',
|
'--middle_name',
|
||||||
action='store',
|
action='store',
|
||||||
dest='middle_name',
|
dest='middle_name',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--last_name',
|
'--last_name',
|
||||||
action='store',
|
action='store',
|
||||||
dest='last_name',
|
dest='last_name',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--suffix',
|
'--suffix',
|
||||||
action='store',
|
action='store',
|
||||||
dest='suffix',
|
dest='suffix',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--salutation',
|
'--salutation',
|
||||||
action='store',
|
action='store',
|
||||||
dest='salutation',
|
dest='salutation',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--address_1',
|
'--address_1',
|
||||||
action='store',
|
action='store',
|
||||||
dest='address_1',
|
dest='address_1',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--address_2',
|
'--address_2',
|
||||||
action='store',
|
action='store',
|
||||||
dest='address_2',
|
dest='address_2',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--address_3',
|
'--address_3',
|
||||||
action='store',
|
action='store',
|
||||||
dest='address_3',
|
dest='address_3',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--city',
|
'--city',
|
||||||
action='store',
|
action='store',
|
||||||
dest='city',
|
dest='city',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--state',
|
'--state',
|
||||||
action='store',
|
action='store',
|
||||||
dest='state',
|
dest='state',
|
||||||
help='Two letter code (e.g. MA)'
|
help='Two letter code (e.g. MA)'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--postal_code',
|
'--postal_code',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -75,12 +76,12 @@ class Command(BaseCommand):
|
|||||||
action='store',
|
action='store',
|
||||||
dest='phone',
|
dest='phone',
|
||||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--extension',
|
'--extension',
|
||||||
action='store',
|
action='store',
|
||||||
dest='extension',
|
dest='extension',
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--phone_country_code',
|
'--phone_country_code',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -92,7 +93,7 @@ class Command(BaseCommand):
|
|||||||
action='store',
|
action='store',
|
||||||
dest='fax',
|
dest='fax',
|
||||||
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
help='Pretty free-form (parens, spaces, dashes), but no country code'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--fax_country_code',
|
'--fax_country_code',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -103,26 +104,26 @@ class Command(BaseCommand):
|
|||||||
'--company_name',
|
'--company_name',
|
||||||
action='store',
|
action='store',
|
||||||
dest='company_name',
|
dest='company_name',
|
||||||
),
|
),
|
||||||
# internal values:
|
# internal values:
|
||||||
make_option(
|
make_option(
|
||||||
'--client_candidate_id',
|
'--client_candidate_id',
|
||||||
action='store',
|
action='store',
|
||||||
dest='client_candidate_id',
|
dest='client_candidate_id',
|
||||||
help='ID we assign a user to identify them to Pearson'
|
help='ID we assign a user to identify them to Pearson'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--upload_status',
|
'--upload_status',
|
||||||
action='store',
|
action='store',
|
||||||
dest='upload_status',
|
dest='upload_status',
|
||||||
help='status value assigned by Pearson'
|
help='status value assigned by Pearson'
|
||||||
),
|
),
|
||||||
make_option(
|
make_option(
|
||||||
'--upload_error_message',
|
'--upload_error_message',
|
||||||
action='store',
|
action='store',
|
||||||
dest='upload_error_message',
|
dest='upload_error_message',
|
||||||
help='error message provided by Pearson on a failure.'
|
help='error message provided by Pearson on a failure.'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
args = "<student_username>"
|
args = "<student_username>"
|
||||||
help = "Create or modify a TestCenterUser entry for a given Student"
|
help = "Create or modify a TestCenterUser entry for a given Student"
|
||||||
@@ -142,20 +143,20 @@ class Command(BaseCommand):
|
|||||||
student = User.objects.get(username=username)
|
student = User.objects.get(username=username)
|
||||||
try:
|
try:
|
||||||
testcenter_user = TestCenterUser.objects.get(user=student)
|
testcenter_user = TestCenterUser.objects.get(user=student)
|
||||||
needs_updating = testcenter_user.needs_update(our_options)
|
needs_updating = testcenter_user.needs_update(our_options)
|
||||||
except TestCenterUser.DoesNotExist:
|
except TestCenterUser.DoesNotExist:
|
||||||
# do additional initialization here:
|
# do additional initialization here:
|
||||||
testcenter_user = TestCenterUser.create(student)
|
testcenter_user = TestCenterUser.create(student)
|
||||||
needs_updating = True
|
needs_updating = True
|
||||||
|
|
||||||
if needs_updating:
|
if needs_updating:
|
||||||
# the registration form normally populates the data dict with
|
# the registration form normally populates the data dict with
|
||||||
# all values from the testcenter_user. But here we only want to
|
# all values from the testcenter_user. But here we only want to
|
||||||
# specify those values that change, so update the dict with existing
|
# specify those values that change, so update the dict with existing
|
||||||
# values.
|
# values.
|
||||||
form_options = dict(our_options)
|
form_options = dict(our_options)
|
||||||
for propname in TestCenterUser.user_provided_fields():
|
for propname in TestCenterUser.user_provided_fields():
|
||||||
if propname not in form_options:
|
if propname not in form_options:
|
||||||
form_options[propname] = testcenter_user.__getattribute__(propname)
|
form_options[propname] = testcenter_user.__getattribute__(propname)
|
||||||
form = TestCenterUserForm(instance=testcenter_user, data=form_options)
|
form = TestCenterUserForm(instance=testcenter_user, data=form_options)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -170,21 +171,20 @@ class Command(BaseCommand):
|
|||||||
errorlist.append("Non-field Form errors encountered:")
|
errorlist.append("Non-field Form errors encountered:")
|
||||||
for nonfielderror in form.non_field_errors:
|
for nonfielderror in form.non_field_errors:
|
||||||
errorlist.append("Non-field Form Error: {}".format(nonfielderror))
|
errorlist.append("Non-field Form Error: {}".format(nonfielderror))
|
||||||
raise CommandError("\n".join(errorlist))
|
raise CommandError("\n".join(errorlist))
|
||||||
else:
|
else:
|
||||||
print "No changes necessary to make to existing user's demographics."
|
print "No changes necessary to make to existing user's demographics."
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
if change_internal:
|
if change_internal:
|
||||||
testcenter_user.save()
|
testcenter_user.save()
|
||||||
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."
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ class Command(BaseCommand):
|
|||||||
if not hasattr(settings, value):
|
if not hasattr(settings, value):
|
||||||
raise CommandError('No entry in the AWS settings'
|
raise CommandError('No entry in the AWS settings'
|
||||||
'(env/auth.json) for {0}'.format(value))
|
'(env/auth.json) for {0}'.format(value))
|
||||||
|
|
||||||
# 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))
|
||||||
@@ -57,9 +57,9 @@ class Command(BaseCommand):
|
|||||||
source_dir = settings.PEARSON['LOCAL_EXPORT']
|
source_dir = settings.PEARSON['LOCAL_EXPORT']
|
||||||
if not os.path.isdir(source_dir):
|
if not os.path.isdir(source_dir):
|
||||||
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))
|
||||||
@@ -76,7 +76,7 @@ class Command(BaseCommand):
|
|||||||
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
|
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
|
||||||
password=settings.PEARSON['SFTP_PASSWORD'])
|
password=settings.PEARSON['SFTP_PASSWORD'])
|
||||||
sftp = paramiko.SFTPClient.from_transport(t)
|
sftp = paramiko.SFTPClient.from_transport(t)
|
||||||
|
|
||||||
if mode == 'export':
|
if mode == 'export':
|
||||||
try:
|
try:
|
||||||
sftp.chdir(files_to)
|
sftp.chdir(files_to)
|
||||||
@@ -92,7 +92,7 @@ class Command(BaseCommand):
|
|||||||
except IOError:
|
except IOError:
|
||||||
raise CommandError('SFTP source path does not exist: {}'.format(files_from))
|
raise CommandError('SFTP source path does not exist: {}'.format(files_from))
|
||||||
for filename in sftp.listdir('.'):
|
for filename in sftp.listdir('.'):
|
||||||
# skip subdirectories
|
# skip subdirectories
|
||||||
if not S_ISDIR(sftp.stat(filename).st_mode):
|
if not S_ISDIR(sftp.stat(filename).st_mode):
|
||||||
sftp.get(filename, files_to + '/' + filename)
|
sftp.get(filename, files_to + '/' + filename)
|
||||||
# delete files from sftp server once they are successfully pulled off:
|
# delete files from sftp server once they are successfully pulled off:
|
||||||
@@ -112,7 +112,7 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
for filename in os.listdir(files_from):
|
for filename in os.listdir(files_from):
|
||||||
source_file = os.path.join(files_from, filename)
|
source_file = os.path.join(files_from, filename)
|
||||||
# use mode as name of directory into which to write files
|
# use mode as name of directory into which to write files
|
||||||
dest_file = os.path.join(mode, filename)
|
dest_file = os.path.join(mode, filename)
|
||||||
upload_file_to_s3(bucket, source_file, dest_file)
|
upload_file_to_s3(bucket, source_file, dest_file)
|
||||||
if deleteAfterCopy:
|
if deleteAfterCopy:
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -17,30 +17,31 @@ from student.models import User, TestCenterRegistration, TestCenterUser, get_tes
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_tc_user(username):
|
def create_tc_user(username):
|
||||||
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||||
options = {
|
options = {
|
||||||
'first_name' : 'TestFirst',
|
'first_name': 'TestFirst',
|
||||||
'last_name' : 'TestLast',
|
'last_name': 'TestLast',
|
||||||
'address_1' : 'Test Address',
|
'address_1': 'Test Address',
|
||||||
'city' : 'TestCity',
|
'city': 'TestCity',
|
||||||
'state' : 'Alberta',
|
'state': 'Alberta',
|
||||||
'postal_code' : 'A0B 1C2',
|
'postal_code': 'A0B 1C2',
|
||||||
'country' : 'CAN',
|
'country': 'CAN',
|
||||||
'phone' : '252-1866',
|
'phone': '252-1866',
|
||||||
'phone_country_code' : '1',
|
'phone_country_code': '1',
|
||||||
}
|
}
|
||||||
call_command('pearson_make_tc_user', username, **options)
|
call_command('pearson_make_tc_user', username, **options)
|
||||||
return TestCenterUser.objects.get(user=user)
|
return TestCenterUser.objects.get(user=user)
|
||||||
|
|
||||||
|
|
||||||
def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code = 'exam1', accommodation_code = None):
|
def create_tc_registration(username, course_id='org1/course1/term1', exam_code='exam1', accommodation_code=None):
|
||||||
|
|
||||||
options = { 'exam_series_code' : exam_code,
|
options = {'exam_series_code': exam_code,
|
||||||
'eligibility_appointment_date_first' : '2013-01-01T00:00',
|
'eligibility_appointment_date_first': '2013-01-01T00:00',
|
||||||
'eligibility_appointment_date_last' : '2013-12-31T23:59',
|
'eligibility_appointment_date_last': '2013-12-31T23:59',
|
||||||
'accommodation_code' : accommodation_code,
|
'accommodation_code': accommodation_code,
|
||||||
'create_dummy_exam' : True,
|
'create_dummy_exam': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
call_command('pearson_make_tc_registration', username, course_id, **options)
|
call_command('pearson_make_tc_registration', username, course_id, **options)
|
||||||
@@ -48,21 +49,23 @@ def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code
|
|||||||
registrations = get_testcenter_registration(user, course_id, exam_code)
|
registrations = get_testcenter_registration(user, course_id, exam_code)
|
||||||
return registrations[0]
|
return registrations[0]
|
||||||
|
|
||||||
|
|
||||||
def create_multiple_registrations(prefix='test'):
|
def create_multiple_registrations(prefix='test'):
|
||||||
username1 = '{}_multiple1'.format(prefix)
|
username1 = '{}_multiple1'.format(prefix)
|
||||||
create_tc_user(username1)
|
create_tc_user(username1)
|
||||||
create_tc_registration(username1)
|
create_tc_registration(username1)
|
||||||
create_tc_registration(username1, course_id = 'org1/course2/term1')
|
create_tc_registration(username1, course_id='org1/course2/term1')
|
||||||
create_tc_registration(username1, exam_code = 'exam2')
|
create_tc_registration(username1, exam_code='exam2')
|
||||||
username2 = '{}_multiple2'.format(prefix)
|
username2 = '{}_multiple2'.format(prefix)
|
||||||
create_tc_user(username2)
|
create_tc_user(username2)
|
||||||
create_tc_registration(username2)
|
create_tc_registration(username2)
|
||||||
username3 = '{}_multiple3'.format(prefix)
|
username3 = '{}_multiple3'.format(prefix)
|
||||||
create_tc_user(username3)
|
create_tc_user(username3)
|
||||||
create_tc_registration(username3, course_id = 'org1/course2/term1')
|
create_tc_registration(username3, course_id='org1/course2/term1')
|
||||||
username4 = '{}_multiple4'.format(prefix)
|
username4 = '{}_multiple4'.format(prefix)
|
||||||
create_tc_user(username4)
|
create_tc_user(username4)
|
||||||
create_tc_registration(username4, exam_code = 'exam2')
|
create_tc_registration(username4, exam_code='exam2')
|
||||||
|
|
||||||
|
|
||||||
def get_command_error_text(*args, **options):
|
def get_command_error_text(*args, **options):
|
||||||
stderr_string = None
|
stderr_string = None
|
||||||
@@ -75,21 +78,22 @@ def get_command_error_text(*args, **options):
|
|||||||
# But these are actually translated into nice messages,
|
# But these are actually translated into nice messages,
|
||||||
# and sys.exit(1) is then called. For testing, we
|
# and sys.exit(1) is then called. For testing, we
|
||||||
# want to catch what sys.exit throws, and get the
|
# want to catch what sys.exit throws, and get the
|
||||||
# relevant text either from stdout or stderr.
|
# relevant text either from stdout or stderr.
|
||||||
if (why1.message > 0):
|
if (why1.message > 0):
|
||||||
stderr_string = sys.stderr.getvalue()
|
stderr_string = sys.stderr.getvalue()
|
||||||
else:
|
else:
|
||||||
raise why1
|
raise why1
|
||||||
except Exception, why:
|
except Exception, why:
|
||||||
raise why
|
raise why
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
if stderr_string is None:
|
if stderr_string is None:
|
||||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||||
return stderr_string
|
return stderr_string
|
||||||
|
|
||||||
|
|
||||||
def get_error_string_for_management_call(*args, **options):
|
def get_error_string_for_management_call(*args, **options):
|
||||||
stdout_string = None
|
stdout_string = None
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
@@ -103,7 +107,7 @@ def get_error_string_for_management_call(*args, **options):
|
|||||||
# But these are actually translated into nice messages,
|
# But these are actually translated into nice messages,
|
||||||
# and sys.exit(1) is then called. For testing, we
|
# and sys.exit(1) is then called. For testing, we
|
||||||
# want to catch what sys.exit throws, and get the
|
# want to catch what sys.exit throws, and get the
|
||||||
# relevant text either from stdout or stderr.
|
# relevant text either from stdout or stderr.
|
||||||
if (why1.message == 1):
|
if (why1.message == 1):
|
||||||
stdout_string = sys.stdout.getvalue()
|
stdout_string = sys.stdout.getvalue()
|
||||||
stderr_string = sys.stderr.getvalue()
|
stderr_string = sys.stderr.getvalue()
|
||||||
@@ -111,15 +115,15 @@ def get_error_string_for_management_call(*args, **options):
|
|||||||
raise why1
|
raise why1
|
||||||
except Exception, why:
|
except Exception, why:
|
||||||
raise why
|
raise why
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
if stdout_string is None:
|
if stdout_string is None:
|
||||||
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
|
||||||
return stdout_string, stderr_string
|
return stdout_string, stderr_string
|
||||||
|
|
||||||
|
|
||||||
def get_file_info(dirpath):
|
def get_file_info(dirpath):
|
||||||
filelist = os.listdir(dirpath)
|
filelist = os.listdir(dirpath)
|
||||||
@@ -132,43 +136,45 @@ def get_file_info(dirpath):
|
|||||||
numlines = len(filecontents)
|
numlines = len(filecontents)
|
||||||
return filepath, numlines
|
return filepath, numlines
|
||||||
else:
|
else:
|
||||||
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath,filelist))
|
raise Exception("Expected to find a single file in {}, but found {}".format(dirpath, filelist))
|
||||||
|
|
||||||
|
|
||||||
class PearsonTestCase(TestCase):
|
class PearsonTestCase(TestCase):
|
||||||
'''
|
'''
|
||||||
Base class for tests running Pearson-related commands
|
Base class for tests running Pearson-related commands
|
||||||
'''
|
'''
|
||||||
import_dir = mkdtemp(prefix="import")
|
import_dir = mkdtemp(prefix="import")
|
||||||
export_dir = mkdtemp(prefix="export")
|
export_dir = mkdtemp(prefix="export")
|
||||||
|
|
||||||
def assertErrorContains(self, error_message, expected):
|
def assertErrorContains(self, error_message, expected):
|
||||||
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
|
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
def delete_temp_dir(dirname):
|
def delete_temp_dir(dirname):
|
||||||
if os.path.exists(dirname):
|
if os.path.exists(dirname):
|
||||||
for filename in os.listdir(dirname):
|
for filename in os.listdir(dirname):
|
||||||
os.remove(os.path.join(dirname, filename))
|
os.remove(os.path.join(dirname, filename))
|
||||||
os.rmdir(dirname)
|
os.rmdir(dirname)
|
||||||
|
|
||||||
# clean up after any test data was dumped to temp directory
|
# clean up after any test data was dumped to temp directory
|
||||||
delete_temp_dir(self.import_dir)
|
delete_temp_dir(self.import_dir)
|
||||||
delete_temp_dir(self.export_dir)
|
delete_temp_dir(self.export_dir)
|
||||||
|
|
||||||
# and clean up the database:
|
# and clean up the database:
|
||||||
# TestCenterUser.objects.all().delete()
|
# TestCenterUser.objects.all().delete()
|
||||||
# TestCenterRegistration.objects.all().delete()
|
# TestCenterRegistration.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class PearsonCommandTestCase(PearsonTestCase):
|
class PearsonCommandTestCase(PearsonTestCase):
|
||||||
|
|
||||||
def test_missing_demographic_fields(self):
|
def test_missing_demographic_fields(self):
|
||||||
# We won't bother to test all details of form validation here.
|
# We won't bother to test all details of form validation here.
|
||||||
# It is enough to show that it works here, but deal with test cases for the form
|
# It is enough to show that it works here, but deal with test cases for the form
|
||||||
# validation in the student tests, not these management tests.
|
# validation in the student tests, not these management tests.
|
||||||
username = 'baduser'
|
username = 'baduser'
|
||||||
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
|
||||||
options = {}
|
options = {}
|
||||||
error_string = get_command_error_text('pearson_make_tc_user', username, **options)
|
error_string = get_command_error_text('pearson_make_tc_user', username, **options)
|
||||||
self.assertTrue(error_string.find('Field Form errors encountered:') >= 0)
|
self.assertTrue(error_string.find('Field Form errors encountered:') >= 0)
|
||||||
self.assertTrue(error_string.find('Field Form Error: city') >= 0)
|
self.assertTrue(error_string.find('Field Form Error: city') >= 0)
|
||||||
self.assertTrue(error_string.find('Field Form Error: first_name') >= 0)
|
self.assertTrue(error_string.find('Field Form Error: first_name') >= 0)
|
||||||
@@ -178,11 +184,11 @@ class PearsonCommandTestCase(PearsonTestCase):
|
|||||||
self.assertTrue(error_string.find('Field Form Error: phone') >= 0)
|
self.assertTrue(error_string.find('Field Form Error: phone') >= 0)
|
||||||
self.assertTrue(error_string.find('Field Form Error: address_1') >= 0)
|
self.assertTrue(error_string.find('Field Form Error: address_1') >= 0)
|
||||||
self.assertErrorContains(error_string, 'Field Form Error: address_1')
|
self.assertErrorContains(error_string, 'Field Form Error: address_1')
|
||||||
|
|
||||||
def test_create_good_testcenter_user(self):
|
def test_create_good_testcenter_user(self):
|
||||||
testcenter_user = create_tc_user("test_good_user")
|
testcenter_user = create_tc_user("test_good_user")
|
||||||
self.assertIsNotNone(testcenter_user)
|
self.assertIsNotNone(testcenter_user)
|
||||||
|
|
||||||
def test_create_good_testcenter_registration(self):
|
def test_create_good_testcenter_registration(self):
|
||||||
username = 'test_good_registration'
|
username = 'test_good_registration'
|
||||||
create_tc_user(username)
|
create_tc_user(username)
|
||||||
@@ -192,21 +198,21 @@ class PearsonCommandTestCase(PearsonTestCase):
|
|||||||
def test_cdd_missing_option(self):
|
def test_cdd_missing_option(self):
|
||||||
error_string = get_command_error_text('pearson_export_cdd', **{})
|
error_string = get_command_error_text('pearson_export_cdd', **{})
|
||||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||||
|
|
||||||
def test_ead_missing_option(self):
|
def test_ead_missing_option(self):
|
||||||
error_string = get_command_error_text('pearson_export_ead', **{})
|
error_string = get_command_error_text('pearson_export_ead', **{})
|
||||||
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
|
||||||
|
|
||||||
def test_export_single_cdd(self):
|
def test_export_single_cdd(self):
|
||||||
# before we generate any tc_users, we expect there to be nothing to output:
|
# before we generate any tc_users, we expect there to be nothing to output:
|
||||||
options = { 'dest-from-settings' : True }
|
options = {'dest-from-settings': True}
|
||||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||||
call_command('pearson_export_cdd', **options)
|
call_command('pearson_export_cdd', **options)
|
||||||
(filepath, numlines) = get_file_info(self.export_dir)
|
(filepath, numlines) = get_file_info(self.export_dir)
|
||||||
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
|
self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
# generating a tc_user should result in a line in the output
|
# generating a tc_user should result in a line in the output
|
||||||
username = 'test_single_cdd'
|
username = 'test_single_cdd'
|
||||||
create_tc_user(username)
|
create_tc_user(username)
|
||||||
call_command('pearson_export_cdd', **options)
|
call_command('pearson_export_cdd', **options)
|
||||||
@@ -221,23 +227,23 @@ class PearsonCommandTestCase(PearsonTestCase):
|
|||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
# if we modify the record, then it should be output again:
|
# if we modify the record, then it should be output again:
|
||||||
user_options = { 'first_name' : 'NewTestFirst', }
|
user_options = {'first_name': 'NewTestFirst', }
|
||||||
call_command('pearson_make_tc_user', username, **user_options)
|
call_command('pearson_make_tc_user', username, **user_options)
|
||||||
call_command('pearson_export_cdd', **options)
|
call_command('pearson_export_cdd', **options)
|
||||||
(filepath, numlines) = get_file_info(self.export_dir)
|
(filepath, numlines) = get_file_info(self.export_dir)
|
||||||
self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
|
self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
def test_export_single_ead(self):
|
def test_export_single_ead(self):
|
||||||
# before we generate any registrations, we expect there to be nothing to output:
|
# before we generate any registrations, we expect there to be nothing to output:
|
||||||
options = { 'dest-from-settings' : True }
|
options = {'dest-from-settings': True}
|
||||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||||
call_command('pearson_export_ead', **options)
|
call_command('pearson_export_ead', **options)
|
||||||
(filepath, numlines) = get_file_info(self.export_dir)
|
(filepath, numlines) = get_file_info(self.export_dir)
|
||||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
# generating a registration should result in a line in the output
|
# generating a registration should result in a line in the output
|
||||||
username = 'test_single_ead'
|
username = 'test_single_ead'
|
||||||
create_tc_user(username)
|
create_tc_user(username)
|
||||||
create_tc_registration(username)
|
create_tc_registration(username)
|
||||||
@@ -251,7 +257,7 @@ class PearsonCommandTestCase(PearsonTestCase):
|
|||||||
(filepath, numlines) = get_file_info(self.export_dir)
|
(filepath, numlines) = get_file_info(self.export_dir)
|
||||||
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
# if we modify the record, then it should be output again:
|
# if we modify the record, then it should be output again:
|
||||||
create_tc_registration(username, accommodation_code='EQPMNT')
|
create_tc_registration(username, accommodation_code='EQPMNT')
|
||||||
call_command('pearson_export_ead', **options)
|
call_command('pearson_export_ead', **options)
|
||||||
@@ -261,8 +267,8 @@ class PearsonCommandTestCase(PearsonTestCase):
|
|||||||
|
|
||||||
def test_export_multiple(self):
|
def test_export_multiple(self):
|
||||||
create_multiple_registrations("export")
|
create_multiple_registrations("export")
|
||||||
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
|
with self.settings(PEARSON={'LOCAL_EXPORT': self.export_dir}):
|
||||||
options = { 'dest-from-settings' : True }
|
options = {'dest-from-settings': True}
|
||||||
call_command('pearson_export_cdd', **options)
|
call_command('pearson_export_cdd', **options)
|
||||||
(filepath, numlines) = get_file_info(self.export_dir)
|
(filepath, numlines) = get_file_info(self.export_dir)
|
||||||
self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
|
self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
|
||||||
@@ -294,6 +300,7 @@ S3_BUCKET = 'edx-pearson-archive'
|
|||||||
AWS_ACCESS_KEY_ID = 'put yours here'
|
AWS_ACCESS_KEY_ID = 'put yours here'
|
||||||
AWS_SECRET_ACCESS_KEY = 'put yours here'
|
AWS_SECRET_ACCESS_KEY = 'put yours here'
|
||||||
|
|
||||||
|
|
||||||
class PearsonTransferTestCase(PearsonTestCase):
|
class PearsonTransferTestCase(PearsonTestCase):
|
||||||
'''
|
'''
|
||||||
Class for tests running Pearson transfers
|
Class for tests running Pearson transfers
|
||||||
@@ -302,14 +309,14 @@ class PearsonTransferTestCase(PearsonTestCase):
|
|||||||
def test_transfer_config(self):
|
def test_transfer_config(self):
|
||||||
with self.settings(DATADOG_API='FAKE_KEY'):
|
with self.settings(DATADOG_API='FAKE_KEY'):
|
||||||
# TODO: why is this failing with the wrong error message?!
|
# TODO: why is this failing with the wrong error message?!
|
||||||
stderrmsg = get_command_error_text('pearson_transfer', **{'mode' : 'garbage'})
|
stderrmsg = get_command_error_text('pearson_transfer', **{'mode': 'garbage'})
|
||||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||||
with self.settings(DATADOG_API='FAKE_KEY'):
|
with self.settings(DATADOG_API='FAKE_KEY'):
|
||||||
stderrmsg = get_command_error_text('pearson_transfer')
|
stderrmsg = get_command_error_text('pearson_transfer')
|
||||||
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
|
||||||
with self.settings(DATADOG_API='FAKE_KEY',
|
with self.settings(DATADOG_API='FAKE_KEY',
|
||||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||||
'LOCAL_IMPORT' : self.import_dir }):
|
'LOCAL_IMPORT': self.import_dir}):
|
||||||
stderrmsg = get_command_error_text('pearson_transfer')
|
stderrmsg = get_command_error_text('pearson_transfer')
|
||||||
self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
|
self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
|
||||||
|
|
||||||
@@ -317,16 +324,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
|||||||
raise SkipTest()
|
raise SkipTest()
|
||||||
create_multiple_registrations('export_missing_dest')
|
create_multiple_registrations('export_missing_dest')
|
||||||
with self.settings(DATADOG_API='FAKE_KEY',
|
with self.settings(DATADOG_API='FAKE_KEY',
|
||||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||||
'SFTP_EXPORT' : 'this/does/not/exist',
|
'SFTP_EXPORT': 'this/does/not/exist',
|
||||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
'SFTP_USERNAME': SFTP_USERNAME,
|
||||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||||
'S3_BUCKET' : S3_BUCKET,
|
'S3_BUCKET': S3_BUCKET,
|
||||||
},
|
},
|
||||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||||
options = { 'mode' : 'export'}
|
options = {'mode': 'export'}
|
||||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||||
self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
|
self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
|
||||||
|
|
||||||
@@ -334,16 +341,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
|||||||
raise SkipTest()
|
raise SkipTest()
|
||||||
create_multiple_registrations("transfer_export")
|
create_multiple_registrations("transfer_export")
|
||||||
with self.settings(DATADOG_API='FAKE_KEY',
|
with self.settings(DATADOG_API='FAKE_KEY',
|
||||||
PEARSON={'LOCAL_EXPORT' : self.export_dir,
|
PEARSON={'LOCAL_EXPORT': self.export_dir,
|
||||||
'SFTP_EXPORT' : 'results/topvue',
|
'SFTP_EXPORT': 'results/topvue',
|
||||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
'SFTP_USERNAME': SFTP_USERNAME,
|
||||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||||
'S3_BUCKET' : S3_BUCKET,
|
'S3_BUCKET': S3_BUCKET,
|
||||||
},
|
},
|
||||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||||
options = { 'mode' : 'export'}
|
options = {'mode': 'export'}
|
||||||
# call_command('pearson_transfer', **options)
|
# call_command('pearson_transfer', **options)
|
||||||
# # confirm that the export directory is still empty:
|
# # confirm that the export directory is still empty:
|
||||||
# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
|
# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
|
||||||
@@ -352,16 +359,16 @@ class PearsonTransferTestCase(PearsonTestCase):
|
|||||||
raise SkipTest()
|
raise SkipTest()
|
||||||
create_multiple_registrations('import_missing_src')
|
create_multiple_registrations('import_missing_src')
|
||||||
with self.settings(DATADOG_API='FAKE_KEY',
|
with self.settings(DATADOG_API='FAKE_KEY',
|
||||||
PEARSON={'LOCAL_IMPORT' : self.import_dir,
|
PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||||
'SFTP_IMPORT' : 'this/does/not/exist',
|
'SFTP_IMPORT': 'this/does/not/exist',
|
||||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
'SFTP_USERNAME': SFTP_USERNAME,
|
||||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||||
'S3_BUCKET' : S3_BUCKET,
|
'S3_BUCKET': S3_BUCKET,
|
||||||
},
|
},
|
||||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||||
options = { 'mode' : 'import'}
|
options = {'mode': 'import'}
|
||||||
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
stderrmsg = get_command_error_text('pearson_transfer', **options)
|
||||||
self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
|
self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
|
||||||
|
|
||||||
@@ -369,15 +376,15 @@ class PearsonTransferTestCase(PearsonTestCase):
|
|||||||
raise SkipTest()
|
raise SkipTest()
|
||||||
create_multiple_registrations('import_missing_src')
|
create_multiple_registrations('import_missing_src')
|
||||||
with self.settings(DATADOG_API='FAKE_KEY',
|
with self.settings(DATADOG_API='FAKE_KEY',
|
||||||
PEARSON={'LOCAL_IMPORT' : self.import_dir,
|
PEARSON={'LOCAL_IMPORT': self.import_dir,
|
||||||
'SFTP_IMPORT' : 'results',
|
'SFTP_IMPORT': 'results',
|
||||||
'SFTP_HOSTNAME' : SFTP_HOSTNAME,
|
'SFTP_HOSTNAME': SFTP_HOSTNAME,
|
||||||
'SFTP_USERNAME' : SFTP_USERNAME,
|
'SFTP_USERNAME': SFTP_USERNAME,
|
||||||
'SFTP_PASSWORD' : SFTP_PASSWORD,
|
'SFTP_PASSWORD': SFTP_PASSWORD,
|
||||||
'S3_BUCKET' : S3_BUCKET,
|
'S3_BUCKET': S3_BUCKET,
|
||||||
},
|
},
|
||||||
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID=AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
|
AWS_SECRET_ACCESS_KEY=AWS_SECRET_ACCESS_KEY):
|
||||||
options = { 'mode' : 'import'}
|
options = {'mode': 'import'}
|
||||||
call_command('pearson_transfer', **options)
|
call_command('pearson_transfer', **options)
|
||||||
self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")
|
self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")
|
||||||
|
|||||||
@@ -185,4 +185,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['student']
|
complete_apps = ['student']
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Migration(SchemaMigration):
|
|||||||
for column in ASKBOT_AUTH_USER_COLUMNS:
|
for column in ASKBOT_AUTH_USER_COLUMNS:
|
||||||
db.delete_column('auth_user', column)
|
db.delete_column('auth_user', column)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print "Couldn't remove askbot because of {0} -- it was probably never here to begin with.".format(ex)
|
print "Couldn't remove askbot because of {0} -- it was probably never here to begin with.".format(ex)
|
||||||
|
|
||||||
def backwards(self, orm):
|
def backwards(self, orm):
|
||||||
raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.")
|
raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.")
|
||||||
|
|||||||
@@ -152,4 +152,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['student']
|
complete_apps = ['student']
|
||||||
|
|||||||
@@ -238,4 +238,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['student']
|
complete_apps = ['student']
|
||||||
|
|||||||
@@ -169,4 +169,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['student']
|
complete_apps = ['student']
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class UserProfile(models.Model):
|
|||||||
TEST_CENTER_STATUS_ACCEPTED = "Accepted"
|
TEST_CENTER_STATUS_ACCEPTED = "Accepted"
|
||||||
TEST_CENTER_STATUS_ERROR = "Error"
|
TEST_CENTER_STATUS_ERROR = "Error"
|
||||||
|
|
||||||
|
|
||||||
class TestCenterUser(models.Model):
|
class TestCenterUser(models.Model):
|
||||||
"""This is our representation of the User for in-person testing, and
|
"""This is our representation of the User for in-person testing, and
|
||||||
specifically for Pearson at this point. A few things to note:
|
specifically for Pearson at this point. A few things to note:
|
||||||
@@ -190,7 +191,7 @@ class TestCenterUser(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def user_provided_fields():
|
def user_provided_fields():
|
||||||
return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
return ['first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
|
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
|
||||||
|
|
||||||
@@ -208,7 +209,7 @@ class TestCenterUser(models.Model):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_edx_id(prefix):
|
def _generate_edx_id(prefix):
|
||||||
NUM_DIGITS = 12
|
NUM_DIGITS = 12
|
||||||
return u"{}{:012}".format(prefix, randint(1, 10**NUM_DIGITS-1))
|
return u"{}{:012}".format(prefix, randint(1, 10 ** NUM_DIGITS - 1))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_candidate_id():
|
def _generate_candidate_id():
|
||||||
@@ -237,10 +238,11 @@ class TestCenterUser(models.Model):
|
|||||||
def is_pending(self):
|
def is_pending(self):
|
||||||
return not self.is_accepted and not self.is_rejected
|
return not self.is_accepted and not self.is_rejected
|
||||||
|
|
||||||
|
|
||||||
class TestCenterUserForm(ModelForm):
|
class TestCenterUserForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TestCenterUser
|
model = TestCenterUser
|
||||||
fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
fields = ('first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
|
||||||
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
|
||||||
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
|
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
|
||||||
|
|
||||||
@@ -313,7 +315,8 @@ ACCOMMODATION_CODES = (
|
|||||||
('SRSGNR', 'Separate Room and Sign Language Interpreter'),
|
('SRSGNR', 'Separate Room and Sign Language Interpreter'),
|
||||||
)
|
)
|
||||||
|
|
||||||
ACCOMMODATION_CODE_DICT = { code : name for (code, name) in ACCOMMODATION_CODES }
|
ACCOMMODATION_CODE_DICT = {code: name for (code, name) in ACCOMMODATION_CODES}
|
||||||
|
|
||||||
|
|
||||||
class TestCenterRegistration(models.Model):
|
class TestCenterRegistration(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -389,10 +392,10 @@ class TestCenterRegistration(models.Model):
|
|||||||
elif self.uploaded_at is None:
|
elif self.uploaded_at is None:
|
||||||
return 'Add'
|
return 'Add'
|
||||||
elif self.registration_is_rejected:
|
elif self.registration_is_rejected:
|
||||||
# Assume that if the registration was rejected before,
|
# Assume that if the registration was rejected before,
|
||||||
# it is more likely this is the (first) correction
|
# it is more likely this is the (first) correction
|
||||||
# than a second correction in flight before the first was
|
# than a second correction in flight before the first was
|
||||||
# processed.
|
# processed.
|
||||||
return 'Add'
|
return 'Add'
|
||||||
else:
|
else:
|
||||||
# TODO: decide what to send when we have uploaded an initial version,
|
# TODO: decide what to send when we have uploaded an initial version,
|
||||||
@@ -417,7 +420,7 @@ class TestCenterRegistration(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, testcenter_user, exam, accommodation_request):
|
def create(cls, testcenter_user, exam, accommodation_request):
|
||||||
registration = cls(testcenter_user = testcenter_user)
|
registration = cls(testcenter_user=testcenter_user)
|
||||||
registration.course_id = exam.course_id
|
registration.course_id = exam.course_id
|
||||||
registration.accommodation_request = accommodation_request.strip()
|
registration.accommodation_request = accommodation_request.strip()
|
||||||
registration.exam_series_code = exam.exam_series_code
|
registration.exam_series_code = exam.exam_series_code
|
||||||
@@ -501,7 +504,7 @@ class TestCenterRegistration(models.Model):
|
|||||||
return self.accommodation_code.split('*')
|
return self.accommodation_code.split('*')
|
||||||
|
|
||||||
def get_accommodation_names(self):
|
def get_accommodation_names(self):
|
||||||
return [ ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes() ]
|
return [ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registration_signup_url(self):
|
def registration_signup_url(self):
|
||||||
@@ -512,7 +515,7 @@ class TestCenterRegistration(models.Model):
|
|||||||
return "Accepted"
|
return "Accepted"
|
||||||
elif self.demographics_is_rejected:
|
elif self.demographics_is_rejected:
|
||||||
return "Rejected"
|
return "Rejected"
|
||||||
else:
|
else:
|
||||||
return "Pending"
|
return "Pending"
|
||||||
|
|
||||||
def accommodation_status(self):
|
def accommodation_status(self):
|
||||||
@@ -522,7 +525,7 @@ class TestCenterRegistration(models.Model):
|
|||||||
return "Accepted"
|
return "Accepted"
|
||||||
elif self.accommodation_is_rejected:
|
elif self.accommodation_is_rejected:
|
||||||
return "Rejected"
|
return "Rejected"
|
||||||
else:
|
else:
|
||||||
return "Pending"
|
return "Pending"
|
||||||
|
|
||||||
def registration_status(self):
|
def registration_status(self):
|
||||||
@@ -532,12 +535,12 @@ class TestCenterRegistration(models.Model):
|
|||||||
return "Rejected"
|
return "Rejected"
|
||||||
else:
|
else:
|
||||||
return "Pending"
|
return "Pending"
|
||||||
|
|
||||||
|
|
||||||
class TestCenterRegistrationForm(ModelForm):
|
class TestCenterRegistrationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TestCenterRegistration
|
model = TestCenterRegistration
|
||||||
fields = ( 'accommodation_request', 'accommodation_code' )
|
fields = ('accommodation_request', 'accommodation_code')
|
||||||
|
|
||||||
def clean_accommodation_request(self):
|
def clean_accommodation_request(self):
|
||||||
code = self.cleaned_data['accommodation_request']
|
code = self.cleaned_data['accommodation_request']
|
||||||
@@ -576,6 +579,7 @@ def get_testcenter_registration(user, course_id, exam_series_code):
|
|||||||
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
|
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
|
||||||
get_testcenter_registration.__test__ = False
|
get_testcenter_registration.__test__ = False
|
||||||
|
|
||||||
|
|
||||||
def unique_id_for_user(user):
|
def unique_id_for_user(user):
|
||||||
"""
|
"""
|
||||||
Return a unique id for a user, suitable for inserting into
|
Return a unique id for a user, suitable for inserting into
|
||||||
@@ -666,6 +670,7 @@ class CourseEnrollmentAllowed(models.Model):
|
|||||||
|
|
||||||
#### Helper methods for use from python manage.py shell and other classes.
|
#### Helper methods for use from python manage.py shell and other classes.
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_username_or_email(username_or_email):
|
def get_user_by_username_or_email(username_or_email):
|
||||||
"""
|
"""
|
||||||
Return a User object, looking up by email if username_or_email contains a
|
Return a User object, looking up by email if username_or_email contains a
|
||||||
@@ -767,4 +772,3 @@ def update_user_information(sender, instance, created, **kwargs):
|
|||||||
log = logging.getLogger("mitx.discussion")
|
log = logging.getLogger("mitx.discussion")
|
||||||
log.error(unicode(e))
|
log.error(unicode(e))
|
||||||
log.error("update user info to discussion failed for user with id: " + str(instance.id))
|
log.error("update user info to discussion failed for user with id: " + str(instance.id))
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ COURSE_2 = 'edx/full/6.002_Spring_2012'
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CourseEndingTest(TestCase):
|
class CourseEndingTest(TestCase):
|
||||||
"""Test things related to course endings: certificates, surveys, etc"""
|
"""Test things related to course endings: certificates, surveys, etc"""
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ class CourseEndingTest(TestCase):
|
|||||||
{'status': 'processing',
|
{'status': 'processing',
|
||||||
'show_disabled_download_button': False,
|
'show_disabled_download_button': False,
|
||||||
'show_download_url': False,
|
'show_download_url': False,
|
||||||
'show_survey_button': False,})
|
'show_survey_button': False, })
|
||||||
|
|
||||||
cert_status = {'status': 'unavailable'}
|
cert_status = {'status': 'unavailable'}
|
||||||
self.assertEqual(_cert_info(user, course, cert_status),
|
self.assertEqual(_cert_info(user, course, cert_status),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from statsd import statsd
|
|||||||
log = logging.getLogger("mitx.student")
|
log = logging.getLogger("mitx.student")
|
||||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||||
|
|
||||||
|
|
||||||
def csrf_token(context):
|
def csrf_token(context):
|
||||||
''' A csrf token that can be included in a form.
|
''' A csrf token that can be included in a form.
|
||||||
'''
|
'''
|
||||||
@@ -73,8 +74,8 @@ def index(request, extra_context={}, user=None):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# The course selection work is done in courseware.courses.
|
# The course selection work is done in courseware.courses.
|
||||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||||
if domain==False: # do explicit check, because domain=None is valid
|
if domain == False: # do explicit check, because domain=None is valid
|
||||||
domain = request.META.get('HTTP_HOST')
|
domain = request.META.get('HTTP_HOST')
|
||||||
|
|
||||||
courses = get_courses(None, domain=domain)
|
courses = get_courses(None, domain=domain)
|
||||||
@@ -97,6 +98,7 @@ import re
|
|||||||
day_pattern = re.compile('\s\d+,\s')
|
day_pattern = re.compile('\s\d+,\s')
|
||||||
multimonth_pattern = re.compile('\s?\-\s?\S+\s')
|
multimonth_pattern = re.compile('\s?\-\s?\S+\s')
|
||||||
|
|
||||||
|
|
||||||
def get_date_for_press(publish_date):
|
def get_date_for_press(publish_date):
|
||||||
import datetime
|
import datetime
|
||||||
# strip off extra months, and just use the first:
|
# strip off extra months, and just use the first:
|
||||||
@@ -107,6 +109,7 @@ def get_date_for_press(publish_date):
|
|||||||
date = datetime.datetime.strptime(date, "%B, %Y")
|
date = datetime.datetime.strptime(date, "%B, %Y")
|
||||||
return date
|
return date
|
||||||
|
|
||||||
|
|
||||||
def press(request):
|
def press(request):
|
||||||
json_articles = cache.get("student_press_json_articles")
|
json_articles = cache.get("student_press_json_articles")
|
||||||
if json_articles == None:
|
if json_articles == None:
|
||||||
@@ -148,6 +151,7 @@ def cert_info(user, course):
|
|||||||
|
|
||||||
return _cert_info(user, course, certificate_status_for_student(user, course.id))
|
return _cert_info(user, course, certificate_status_for_student(user, course.id))
|
||||||
|
|
||||||
|
|
||||||
def _cert_info(user, course, cert_status):
|
def _cert_info(user, course, cert_status):
|
||||||
"""
|
"""
|
||||||
Implements the logic for cert_info -- split out for testing.
|
Implements the logic for cert_info -- split out for testing.
|
||||||
@@ -175,7 +179,7 @@ def _cert_info(user, course, cert_status):
|
|||||||
|
|
||||||
d = {'status': status,
|
d = {'status': status,
|
||||||
'show_download_url': status == 'ready',
|
'show_download_url': status == 'ready',
|
||||||
'show_disabled_download_button': status == 'generating',}
|
'show_disabled_download_button': status == 'generating', }
|
||||||
|
|
||||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||||
course.end_of_course_survey_url is not None):
|
course.end_of_course_survey_url is not None):
|
||||||
@@ -204,6 +208,7 @@ def _cert_info(user, course, cert_status):
|
|||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
@@ -237,9 +242,9 @@ def dashboard(request):
|
|||||||
show_courseware_links_for = frozenset(course.id for course in courses
|
show_courseware_links_for = frozenset(course.id for course in courses
|
||||||
if has_access(request.user, course, 'load'))
|
if has_access(request.user, course, 'load'))
|
||||||
|
|
||||||
cert_statuses = { course.id: cert_info(request.user, course) for course in courses}
|
cert_statuses = {course.id: cert_info(request.user, course) for course in courses}
|
||||||
|
|
||||||
exam_registrations = { course.id: exam_registration_info(request.user, course) for course in courses}
|
exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
|
||||||
|
|
||||||
# Get the 3 most recent news
|
# Get the 3 most recent news
|
||||||
top_news = _get_news(top=3)
|
top_news = _get_news(top=3)
|
||||||
@@ -248,7 +253,7 @@ def dashboard(request):
|
|||||||
'message': message,
|
'message': message,
|
||||||
'staff_access': staff_access,
|
'staff_access': staff_access,
|
||||||
'errored_courses': errored_courses,
|
'errored_courses': errored_courses,
|
||||||
'show_courseware_links_for' : show_courseware_links_for,
|
'show_courseware_links_for': show_courseware_links_for,
|
||||||
'cert_statuses': cert_statuses,
|
'cert_statuses': cert_statuses,
|
||||||
'news': top_news,
|
'news': top_news,
|
||||||
'exam_registrations': exam_registrations,
|
'exam_registrations': exam_registrations,
|
||||||
@@ -312,7 +317,7 @@ def change_enrollment(request):
|
|||||||
'error': 'enrollment in {} not allowed at this time'
|
'error': 'enrollment in {} not allowed at this time'
|
||||||
.format(course.display_name)}
|
.format(course.display_name)}
|
||||||
|
|
||||||
org, course_num, run=course_id.split("/")
|
org, course_num, run = course_id.split("/")
|
||||||
statsd.increment("common.student.enrollment",
|
statsd.increment("common.student.enrollment",
|
||||||
tags=["org:{0}".format(org),
|
tags=["org:{0}".format(org),
|
||||||
"course:{0}".format(course_num),
|
"course:{0}".format(course_num),
|
||||||
@@ -326,7 +331,7 @@ def change_enrollment(request):
|
|||||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||||
enrollment.delete()
|
enrollment.delete()
|
||||||
|
|
||||||
org, course_num, run=course_id.split("/")
|
org, course_num, run = course_id.split("/")
|
||||||
statsd.increment("common.student.unenrollment",
|
statsd.increment("common.student.unenrollment",
|
||||||
tags=["org:{0}".format(org),
|
tags=["org:{0}".format(org),
|
||||||
"course:{0}".format(course_num),
|
"course:{0}".format(course_num),
|
||||||
@@ -345,7 +350,7 @@ def change_enrollment(request):
|
|||||||
def accounts_login(request, error=""):
|
def accounts_login(request, error=""):
|
||||||
|
|
||||||
|
|
||||||
return render_to_response('accounts_login.html', { 'error': error })
|
return render_to_response('accounts_login.html', {'error': error})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -424,6 +429,7 @@ def change_setting(request):
|
|||||||
return HttpResponse(json.dumps({'success': True,
|
return HttpResponse(json.dumps({'success': True,
|
||||||
'location': up.location, }))
|
'location': up.location, }))
|
||||||
|
|
||||||
|
|
||||||
def _do_create_account(post_vars):
|
def _do_create_account(post_vars):
|
||||||
"""
|
"""
|
||||||
Given cleaned post variables, create the User and UserProfile objects, as well as the
|
Given cleaned post variables, create the User and UserProfile objects, as well as the
|
||||||
@@ -551,7 +557,7 @@ def create_account(request, post_override=None):
|
|||||||
|
|
||||||
# Ok, looks like everything is legit. Create the account.
|
# Ok, looks like everything is legit. Create the account.
|
||||||
ret = _do_create_account(post_vars)
|
ret = _do_create_account(post_vars)
|
||||||
if isinstance(ret,HttpResponse): # if there was an error then return that
|
if isinstance(ret, HttpResponse): # if there was an error then return that
|
||||||
return ret
|
return ret
|
||||||
(user, profile, registration) = ret
|
(user, profile, registration) = ret
|
||||||
|
|
||||||
@@ -591,7 +597,7 @@ def create_account(request, post_override=None):
|
|||||||
eamap.user = login_user
|
eamap.user = login_user
|
||||||
eamap.dtsignup = datetime.datetime.now()
|
eamap.dtsignup = datetime.datetime.now()
|
||||||
eamap.save()
|
eamap.save()
|
||||||
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'],eamap))
|
log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap))
|
||||||
|
|
||||||
if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
|
if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'):
|
||||||
log.debug('bypassing activation email')
|
log.debug('bypassing activation email')
|
||||||
@@ -603,6 +609,7 @@ def create_account(request, post_override=None):
|
|||||||
js = {'success': True}
|
js = {'success': True}
|
||||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
def exam_registration_info(user, course):
|
def exam_registration_info(user, course):
|
||||||
""" Returns a Registration object if the user is currently registered for a current
|
""" Returns a Registration object if the user is currently registered for a current
|
||||||
exam of the course. Returns None if the user is not registered, or if there is no
|
exam of the course. Returns None if the user is not registered, or if there is no
|
||||||
@@ -620,6 +627,7 @@ def exam_registration_info(user, course):
|
|||||||
registration = None
|
registration = None
|
||||||
return registration
|
return registration
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def begin_exam_registration(request, course_id):
|
def begin_exam_registration(request, course_id):
|
||||||
@@ -663,6 +671,7 @@ def begin_exam_registration(request, course_id):
|
|||||||
|
|
||||||
return render_to_response('test_center_register.html', context)
|
return render_to_response('test_center_register.html', context)
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def create_exam_registration(request, post_override=None):
|
def create_exam_registration(request, post_override=None):
|
||||||
'''
|
'''
|
||||||
@@ -725,7 +734,7 @@ def create_exam_registration(request, post_override=None):
|
|||||||
# this registration screen.
|
# this registration screen.
|
||||||
|
|
||||||
else:
|
else:
|
||||||
accommodation_request = post_vars.get('accommodation_request','')
|
accommodation_request = post_vars.get('accommodation_request', '')
|
||||||
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
registration = TestCenterRegistration.create(testcenter_user, exam, accommodation_request)
|
||||||
needs_saving = True
|
needs_saving = True
|
||||||
log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id))
|
log.info("User {0} enrolled in course {1} creating new exam registration".format(user.username, course_id))
|
||||||
@@ -834,16 +843,17 @@ def password_reset(request):
|
|||||||
|
|
||||||
form = PasswordResetForm(request.POST)
|
form = PasswordResetForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save(use_https = request.is_secure(),
|
form.save(use_https=request.is_secure(),
|
||||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
request = request,
|
request=request,
|
||||||
domain_override = request.get_host())
|
domain_override=request.get_host())
|
||||||
return HttpResponse(json.dumps({'success':True,
|
return HttpResponse(json.dumps({'success': True,
|
||||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||||
else:
|
else:
|
||||||
return HttpResponse(json.dumps({'success': False,
|
return HttpResponse(json.dumps({'success': False,
|
||||||
'error': 'Invalid e-mail'}))
|
'error': 'Invalid e-mail'}))
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def reactivation_email(request):
|
def reactivation_email(request):
|
||||||
''' Send an e-mail to reactivate a deactivated account, or to
|
''' Send an e-mail to reactivate a deactivated account, or to
|
||||||
@@ -856,6 +866,7 @@ def reactivation_email(request):
|
|||||||
'error': 'No inactive user with this e-mail exists'}))
|
'error': 'No inactive user with this e-mail exists'}))
|
||||||
return reactivation_email_for_user(user)
|
return reactivation_email_for_user(user)
|
||||||
|
|
||||||
|
|
||||||
def reactivation_email_for_user(user):
|
def reactivation_email_for_user(user):
|
||||||
reg = Registration.objects.get(user=user)
|
reg = Registration.objects.get(user=user)
|
||||||
|
|
||||||
@@ -996,11 +1007,11 @@ def pending_name_changes(request):
|
|||||||
|
|
||||||
changes = list(PendingNameChange.objects.all())
|
changes = list(PendingNameChange.objects.all())
|
||||||
js = {'students': [{'new_name': c.new_name,
|
js = {'students': [{'new_name': c.new_name,
|
||||||
'rationale':c.rationale,
|
'rationale': c.rationale,
|
||||||
'old_name':UserProfile.objects.get(user=c.user).name,
|
'old_name': UserProfile.objects.get(user=c.user).name,
|
||||||
'email':c.user.email,
|
'email': c.user.email,
|
||||||
'uid':c.user.id,
|
'uid': c.user.id,
|
||||||
'cid':c.id} for c in changes]}
|
'cid': c.id} for c in changes]}
|
||||||
return render_to_response('name_changes.html', js)
|
return render_to_response('name_changes.html', js)
|
||||||
|
|
||||||
|
|
||||||
@@ -1057,6 +1068,8 @@ def accept_name_change(request):
|
|||||||
|
|
||||||
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
|
# TODO: This is a giant kludge to give Pearson something to test against ASAP.
|
||||||
# Will need to get replaced by something that actually ties into TestCenterUser
|
# Will need to get replaced by something that actually ties into TestCenterUser
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def test_center_login(request):
|
def test_center_login(request):
|
||||||
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
|
if not settings.MITX_FEATURES.get('ENABLE_PEARSON_HACK_TEST'):
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['track']
|
complete_apps = ['track']
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ class Migration(SchemaMigration):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete_apps = ['track']
|
complete_apps = ['track']
|
||||||
|
|||||||
@@ -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
|
|||||||
|
|
||||||
log = logging.getLogger("tracking")
|
log = logging.getLogger("tracking")
|
||||||
|
|
||||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host']
|
LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', 'page', 'time', 'host']
|
||||||
|
|
||||||
|
|
||||||
def log_event(event):
|
def log_event(event):
|
||||||
event_str = json.dumps(event)
|
event_str = json.dumps(event)
|
||||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||||
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||||
event['time'] = dateutil.parser.parse(event['time'])
|
event['time'] = dateutil.parser.parse(event['time'])
|
||||||
tldat = TrackingLog(**dict( (x,event[x]) for x in LOGFIELDS ))
|
tldat = TrackingLog(**dict((x, event[x]) for x in LOGFIELDS))
|
||||||
try:
|
try:
|
||||||
tldat.save()
|
tldat.save()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.exception(err)
|
log.exception(err)
|
||||||
|
|
||||||
|
|
||||||
def user_track(request):
|
def user_track(request):
|
||||||
try: # TODO: Do the same for many of the optional META parameters
|
try: # TODO: Do the same for many of the optional META parameters
|
||||||
username = request.user.username
|
username = request.user.username
|
||||||
@@ -87,13 +89,14 @@ def server_track(request, event_type, event, page=None):
|
|||||||
"host": request.META['SERVER_NAME'],
|
"host": request.META['SERVER_NAME'],
|
||||||
}
|
}
|
||||||
|
|
||||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||||
return
|
return
|
||||||
log_event(event)
|
log_event(event)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def view_tracking_log(request,args=''):
|
def view_tracking_log(request, args=''):
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
nlen = 100
|
nlen = 100
|
||||||
@@ -104,16 +107,15 @@ def view_tracking_log(request,args=''):
|
|||||||
nlen = int(arg)
|
nlen = int(arg)
|
||||||
if arg.startswith('username='):
|
if arg.startswith('username='):
|
||||||
username = arg[9:]
|
username = arg[9:]
|
||||||
|
|
||||||
record_instances = TrackingLog.objects.all().order_by('-time')
|
record_instances = TrackingLog.objects.all().order_by('-time')
|
||||||
if username:
|
if username:
|
||||||
record_instances = record_instances.filter(username=username)
|
record_instances = record_instances.filter(username=username)
|
||||||
record_instances = record_instances[0:nlen]
|
record_instances = record_instances[0:nlen]
|
||||||
|
|
||||||
# fix dtstamp
|
# fix dtstamp
|
||||||
fmt = '%a %d-%b-%y %H:%M:%S' # "%Y-%m-%d %H:%M:%S %Z%z"
|
fmt = '%a %d-%b-%y %H:%M:%S' # "%Y-%m-%d %H:%M:%S %Z%z"
|
||||||
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):
|
|||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
return _decorated
|
return _decorated
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import time, datetime
|
import time
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
|
||||||
def time_to_date(time_obj):
|
def time_to_date(time_obj):
|
||||||
"""
|
"""
|
||||||
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
|
Convert a time.time_struct to a true universal time (can pass to js Date constructor)
|
||||||
@@ -9,6 +11,7 @@ def time_to_date(time_obj):
|
|||||||
# TODO change to using the isoformat() function on datetime. js date can parse those
|
# TODO change to using the isoformat() function on datetime. js date can parse those
|
||||||
return calendar.timegm(time_obj) * 1000
|
return calendar.timegm(time_obj) * 1000
|
||||||
|
|
||||||
|
|
||||||
def jsdate_to_time(field):
|
def jsdate_to_time(field):
|
||||||
"""
|
"""
|
||||||
Convert a universal time (iso format) or msec since epoch to a time obj
|
Convert a universal time (iso format) or msec since epoch to a time obj
|
||||||
@@ -16,9 +19,9 @@ def jsdate_to_time(field):
|
|||||||
if field is None:
|
if field is None:
|
||||||
return field
|
return field
|
||||||
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
|
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
|
||||||
d=datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
|
||||||
return d.utctimetuple()
|
return d.utctimetuple()
|
||||||
elif isinstance(field, int) or isinstance(field, float):
|
elif isinstance(field, int) or isinstance(field, float):
|
||||||
return time.gmtime(field / 1000)
|
return time.gmtime(field / 1000)
|
||||||
elif isinstance(field, time.struct_time):
|
elif isinstance(field, time.struct_time):
|
||||||
return field
|
return field
|
||||||
|
|||||||
@@ -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):
|
|||||||
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
|
|||||||
|
|
||||||
log = logging.getLogger("mitx.xmodule_modifiers")
|
log = logging.getLogger("mitx.xmodule_modifiers")
|
||||||
|
|
||||||
|
|
||||||
def wrap_xmodule(get_html, module, template, context=None):
|
def wrap_xmodule(get_html, module, template, context=None):
|
||||||
"""
|
"""
|
||||||
Wraps the results of get_html in a standard <section> with identifying
|
Wraps the results of get_html in a standard <section> with identifying
|
||||||
@@ -32,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
|
|||||||
def _get_html():
|
def _get_html():
|
||||||
context.update({
|
context.update({
|
||||||
'content': get_html(),
|
'content': get_html(),
|
||||||
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
|
'display_name': module.metadata.get('display_name') if module.metadata is not None else None,
|
||||||
'class_': module.__class__.__name__,
|
'class_': module.__class__.__name__,
|
||||||
'module_name': module.js_module_name
|
'module_name': module.js_module_name
|
||||||
})
|
})
|
||||||
@@ -52,6 +53,7 @@ def replace_course_urls(get_html, course_id):
|
|||||||
return static_replace.replace_course_urls(get_html(), course_id)
|
return static_replace.replace_course_urls(get_html(), course_id)
|
||||||
return _get_html
|
return _get_html
|
||||||
|
|
||||||
|
|
||||||
def replace_static_urls(get_html, data_dir, course_namespace=None):
|
def replace_static_urls(get_html, data_dir, course_namespace=None):
|
||||||
"""
|
"""
|
||||||
Updates the supplied module with a new get_html function that wraps
|
Updates the supplied module with a new get_html function that wraps
|
||||||
@@ -64,6 +66,7 @@ def replace_static_urls(get_html, data_dir, course_namespace=None):
|
|||||||
return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
|
return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
|
||||||
return _get_html
|
return _get_html
|
||||||
|
|
||||||
|
|
||||||
def grade_histogram(module_id):
|
def grade_histogram(module_id):
|
||||||
''' Print out a histogram of grades on a given problem.
|
''' Print out a histogram of grades on a given problem.
|
||||||
Part of staff member debug info.
|
Part of staff member debug info.
|
||||||
@@ -98,7 +101,7 @@ def add_histogram(get_html, module, user):
|
|||||||
@wraps(get_html)
|
@wraps(get_html)
|
||||||
def _get_html():
|
def _get_html():
|
||||||
|
|
||||||
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
|
if type(module) in [SequenceModule, VerticalModule]: # TODO: make this more general, eg use an XModule attribute instead
|
||||||
return get_html()
|
return get_html()
|
||||||
|
|
||||||
module_id = module.id
|
module_id = module.id
|
||||||
@@ -114,35 +117,35 @@ def add_histogram(get_html, module, user):
|
|||||||
# doesn't like symlinks)
|
# doesn't like symlinks)
|
||||||
filepath = filename
|
filepath = filename
|
||||||
data_dir = osfs.root_path.rsplit('/')[-1]
|
data_dir = osfs.root_path.rsplit('/')[-1]
|
||||||
giturl = module.metadata.get('giturl','https://github.com/MITx')
|
giturl = module.metadata.get('giturl', 'https://github.com/MITx')
|
||||||
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
|
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
|
||||||
else:
|
else:
|
||||||
edit_link = False
|
edit_link = False
|
||||||
# Need to define all the variables that are about to be used
|
# Need to define all the variables that are about to be used
|
||||||
giturl = ""
|
giturl = ""
|
||||||
data_dir = ""
|
data_dir = ""
|
||||||
source_file = module.metadata.get('source_file','') # source used to generate the problem XML, eg latex or word
|
source_file = module.metadata.get('source_file', '') # source used to generate the problem XML, eg latex or word
|
||||||
|
|
||||||
# useful to indicate to staff if problem has been released or not
|
# useful to indicate to staff if problem has been released or not
|
||||||
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
|
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
|
||||||
now = time.gmtime()
|
now = time.gmtime()
|
||||||
is_released = "unknown"
|
is_released = "unknown"
|
||||||
mstart = getattr(module.descriptor,'start')
|
mstart = getattr(module.descriptor, 'start')
|
||||||
if mstart is not None:
|
if mstart is not None:
|
||||||
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
|
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
|
||||||
|
|
||||||
staff_context = {'definition': module.definition.get('data'),
|
staff_context = {'definition': module.definition.get('data'),
|
||||||
'metadata': json.dumps(module.metadata, indent=4),
|
'metadata': json.dumps(module.metadata, indent=4),
|
||||||
'location': module.location,
|
'location': module.location,
|
||||||
'xqa_key': module.metadata.get('xqa_key',''),
|
'xqa_key': module.metadata.get('xqa_key', ''),
|
||||||
'source_file' : source_file,
|
'source_file': source_file,
|
||||||
'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file),
|
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
|
||||||
'category': str(module.__class__.__name__),
|
'category': str(module.__class__.__name__),
|
||||||
# Template uses element_id in js function names, so can't allow dashes
|
# Template uses element_id in js function names, so can't allow dashes
|
||||||
'element_id': module.location.html_id().replace('-','_'),
|
'element_id': module.location.html_id().replace('-', '_'),
|
||||||
'edit_link': edit_link,
|
'edit_link': edit_link,
|
||||||
'user': user,
|
'user': user,
|
||||||
'xqa_server' : settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa'),
|
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
|
||||||
'histogram': json.dumps(histogram),
|
'histogram': json.dumps(histogram),
|
||||||
'render_histogram': render_histogram,
|
'render_histogram': render_histogram,
|
||||||
'module_content': get_html(),
|
'module_content': get_html(),
|
||||||
@@ -151,4 +154,3 @@ def add_histogram(get_html, module, user):
|
|||||||
return render_to_string("staff_problem_info.html", staff_context)
|
return render_to_string("staff_problem_info.html", staff_context)
|
||||||
|
|
||||||
return _get_html
|
return _get_html
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||||
# 5R instead of 5*R
|
# 5R instead of 5*R
|
||||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||||
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||||
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||||
|
|
||||||
def super_float(text):
|
def super_float(text):
|
||||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ global_context = {'random': random,
|
|||||||
'draganddrop': verifiers.draganddrop}
|
'draganddrop': verifiers.draganddrop}
|
||||||
|
|
||||||
# These should be removed from HTML output, including all subelements
|
# These should be removed from HTML output, including all subelements
|
||||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
|
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
|
||||||
|
|
||||||
log = logging.getLogger('mitx.' + __name__)
|
log = logging.getLogger('mitx.' + __name__)
|
||||||
|
|
||||||
@@ -453,7 +453,7 @@ class LoncapaProblem(object):
|
|||||||
exec code in context, context
|
exec code in context, context
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.exception("Error while execing script code: " + code)
|
log.exception("Error while execing script code: " + code)
|
||||||
msg = "Error while executing script code: %s" % str(err).replace('<','<')
|
msg = "Error while executing script code: %s" % str(err).replace('<', '<')
|
||||||
raise responsetypes.LoncapaProblemError(msg)
|
raise responsetypes.LoncapaProblemError(msg)
|
||||||
finally:
|
finally:
|
||||||
sys.path = original_path
|
sys.path = original_path
|
||||||
@@ -502,7 +502,7 @@ class LoncapaProblem(object):
|
|||||||
'id': problemtree.get('id'),
|
'id': problemtree.get('id'),
|
||||||
'feedback': {'message': msg,
|
'feedback': {'message': msg,
|
||||||
'hint': hint,
|
'hint': hint,
|
||||||
'hintmode': hintmode,}}
|
'hintmode': hintmode, }}
|
||||||
|
|
||||||
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
||||||
the_input = input_type_cls(self.system, problemtree, state)
|
the_input = input_type_cls(self.system, problemtree, state)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ from nltk.tree import Tree
|
|||||||
ARROWS = ('<->', '->')
|
ARROWS = ('<->', '->')
|
||||||
|
|
||||||
## Defines a simple pyparsing tokenizer for chemical equations
|
## Defines a simple pyparsing tokenizer for chemical equations
|
||||||
elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be',
|
elements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be',
|
||||||
'Bh','Bi','Bk','Br','C','Ca','Cd','Ce','Cf','Cl','Cm',
|
'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm',
|
||||||
'Cn','Co','Cr','Cs','Cu','Db','Ds','Dy','Er','Es','Eu',
|
'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu',
|
||||||
'F','Fe','Fl','Fm','Fr','Ga','Gd','Ge','H','He','Hf',
|
'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf',
|
||||||
'Hg','Ho','Hs','I','In','Ir','K','Kr','La','Li','Lr',
|
'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr',
|
||||||
'Lu','Lv','Md','Mg','Mn','Mo','Mt','N','Na','Nb','Nd',
|
'Lu', 'Lv', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd',
|
||||||
'Ne','Ni','No','Np','O','Os','P','Pa','Pb','Pd','Pm',
|
'Ne', 'Ni', 'No', 'Np', 'O', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm',
|
||||||
'Po','Pr','Pt','Pu','Ra','Rb','Re','Rf','Rg','Rh','Rn',
|
'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn',
|
||||||
'Ru','S','Sb','Sc','Se','Sg','Si','Sm','Sn','Sr','Ta',
|
'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta',
|
||||||
'Tb','Tc','Te','Th','Ti','Tl','Tm','U','Uuo','Uup',
|
'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'U', 'Uuo', 'Uup',
|
||||||
'Uus','Uut','V','W','Xe','Y','Yb','Zn','Zr']
|
'Uus', 'Uut', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr']
|
||||||
digits = map(str, range(10))
|
digits = map(str, range(10))
|
||||||
symbols = list("[](){}^+-/")
|
symbols = list("[](){}^+-/")
|
||||||
phases = ["(s)", "(l)", "(g)", "(aq)"]
|
phases = ["(s)", "(l)", "(g)", "(aq)"]
|
||||||
@@ -252,7 +252,7 @@ def _get_final_tree(s):
|
|||||||
'''
|
'''
|
||||||
tokenized = tokenizer.parseString(s)
|
tokenized = tokenizer.parseString(s)
|
||||||
parsed = parser.parse(tokenized)
|
parsed = parser.parse(tokenized)
|
||||||
merged = _merge_children(parsed, {'S','group'})
|
merged = _merge_children(parsed, {'S', 'group'})
|
||||||
final = _clean_parse_tree(merged)
|
final = _clean_parse_tree(merged)
|
||||||
return final
|
return final
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Used by responsetypes and capa_problem
|
# Used by responsetypes and capa_problem
|
||||||
|
|
||||||
|
|
||||||
class CorrectMap(object):
|
class CorrectMap(object):
|
||||||
"""
|
"""
|
||||||
Stores map between answer_id and response evaluation result for each question
|
Stores map between answer_id and response evaluation result for each question
|
||||||
@@ -152,6 +153,3 @@ class CorrectMap(object):
|
|||||||
if not isinstance(other_cmap, CorrectMap):
|
if not isinstance(other_cmap, CorrectMap):
|
||||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||||
self.cmap.update(other_cmap.get_dict())
|
self.cmap.update(other_cmap.get_dict())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ log = logging.getLogger('mitx.' + __name__)
|
|||||||
registry = TagRegistry()
|
registry = TagRegistry()
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class MathRenderer(object):
|
class MathRenderer(object):
|
||||||
tags = ['math']
|
tags = ['math']
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ registry.register(MathRenderer)
|
|||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class SolutionRenderer(object):
|
class SolutionRenderer(object):
|
||||||
'''
|
'''
|
||||||
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
|
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
|
||||||
@@ -97,4 +100,3 @@ class SolutionRenderer(object):
|
|||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
registry.register(SolutionRenderer)
|
registry.register(SolutionRenderer)
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ log = logging.getLogger('mitx.' + __name__)
|
|||||||
|
|
||||||
registry = TagRegistry()
|
registry = TagRegistry()
|
||||||
|
|
||||||
|
|
||||||
class Attribute(object):
|
class Attribute(object):
|
||||||
"""
|
"""
|
||||||
Allows specifying required and optional attributes for input types.
|
Allows specifying required and optional attributes for input types.
|
||||||
@@ -413,7 +414,7 @@ class JavascriptInput(InputTypeBase):
|
|||||||
return [Attribute('params', None),
|
return [Attribute('params', None),
|
||||||
Attribute('problem_state', None),
|
Attribute('problem_state', None),
|
||||||
Attribute('display_class', None),
|
Attribute('display_class', None),
|
||||||
Attribute('display_file', None),]
|
Attribute('display_file', None), ]
|
||||||
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
@@ -477,12 +478,13 @@ class TextLine(InputTypeBase):
|
|||||||
|
|
||||||
def _extra_context(self):
|
def _extra_context(self):
|
||||||
return {'do_math': self.do_math,
|
return {'do_math': self.do_math,
|
||||||
'preprocessor': self.preprocessor,}
|
'preprocessor': self.preprocessor, }
|
||||||
|
|
||||||
registry.register(TextLine)
|
registry.register(TextLine)
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class FileSubmission(InputTypeBase):
|
class FileSubmission(InputTypeBase):
|
||||||
"""
|
"""
|
||||||
Upload some files (e.g. for programming assignments)
|
Upload some files (e.g. for programming assignments)
|
||||||
@@ -508,7 +510,7 @@ class FileSubmission(InputTypeBase):
|
|||||||
Convert the list of allowed files to a convenient format.
|
Convert the list of allowed files to a convenient format.
|
||||||
"""
|
"""
|
||||||
return [Attribute('allowed_files', '[]', transform=cls.parse_files),
|
return [Attribute('allowed_files', '[]', transform=cls.parse_files),
|
||||||
Attribute('required_files', '[]', transform=cls.parse_files),]
|
Attribute('required_files', '[]', transform=cls.parse_files), ]
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""
|
"""
|
||||||
@@ -524,7 +526,7 @@ class FileSubmission(InputTypeBase):
|
|||||||
self.msg = FileSubmission.submitted_msg
|
self.msg = FileSubmission.submitted_msg
|
||||||
|
|
||||||
def _extra_context(self):
|
def _extra_context(self):
|
||||||
return {'queue_len': self.queue_len,}
|
return {'queue_len': self.queue_len, }
|
||||||
return context
|
return context
|
||||||
|
|
||||||
registry.register(FileSubmission)
|
registry.register(FileSubmission)
|
||||||
@@ -582,7 +584,7 @@ class CodeInput(InputTypeBase):
|
|||||||
|
|
||||||
def _extra_context(self):
|
def _extra_context(self):
|
||||||
"""Defined queue_len, add it """
|
"""Defined queue_len, add it """
|
||||||
return {'queue_len': self.queue_len,}
|
return {'queue_len': self.queue_len, }
|
||||||
|
|
||||||
registry.register(CodeInput)
|
registry.register(CodeInput)
|
||||||
|
|
||||||
@@ -606,7 +608,7 @@ class Schematic(InputTypeBase):
|
|||||||
Attribute('parts', None),
|
Attribute('parts', None),
|
||||||
Attribute('analyses', None),
|
Attribute('analyses', None),
|
||||||
Attribute('initial_value', None),
|
Attribute('initial_value', None),
|
||||||
Attribute('submit_analyses', None),]
|
Attribute('submit_analyses', None), ]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -614,6 +616,7 @@ registry.register(Schematic)
|
|||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class ImageInput(InputTypeBase):
|
class ImageInput(InputTypeBase):
|
||||||
"""
|
"""
|
||||||
Clickable image as an input field. Element should specify the image source, height,
|
Clickable image as an input field. Element should specify the image source, height,
|
||||||
@@ -635,7 +638,7 @@ class ImageInput(InputTypeBase):
|
|||||||
"""
|
"""
|
||||||
return [Attribute('src'),
|
return [Attribute('src'),
|
||||||
Attribute('height'),
|
Attribute('height'),
|
||||||
Attribute('width'),]
|
Attribute('width'), ]
|
||||||
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
@@ -660,6 +663,7 @@ registry.register(ImageInput)
|
|||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class Crystallography(InputTypeBase):
|
class Crystallography(InputTypeBase):
|
||||||
"""
|
"""
|
||||||
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
|
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
|
||||||
@@ -728,18 +732,19 @@ class ChemicalEquationInput(InputTypeBase):
|
|||||||
"""
|
"""
|
||||||
Can set size of text field.
|
Can set size of text field.
|
||||||
"""
|
"""
|
||||||
return [Attribute('size', '20'),]
|
return [Attribute('size', '20'), ]
|
||||||
|
|
||||||
def _extra_context(self):
|
def _extra_context(self):
|
||||||
"""
|
"""
|
||||||
TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded.
|
TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded.
|
||||||
"""
|
"""
|
||||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js',}
|
return {'previewer': '/static/js/capa/chemical_equation_preview.js', }
|
||||||
|
|
||||||
registry.register(ChemicalEquationInput)
|
registry.register(ChemicalEquationInput)
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class DragAndDropInput(InputTypeBase):
|
class DragAndDropInput(InputTypeBase):
|
||||||
"""
|
"""
|
||||||
Input for drag and drop problems. Allows student to drag and drop images and
|
Input for drag and drop problems. Allows student to drag and drop images and
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ class LoncapaResponse(object):
|
|||||||
tree = etree.Element('span')
|
tree = etree.Element('span')
|
||||||
|
|
||||||
# problem author can make this span display:inline
|
# problem author can make this span display:inline
|
||||||
if self.xml.get('inline',''):
|
if self.xml.get('inline', ''):
|
||||||
tree.set('class','inline')
|
tree.set('class', 'inline')
|
||||||
|
|
||||||
for item in self.xml:
|
for item in self.xml:
|
||||||
# call provided procedure to do the rendering
|
# call provided procedure to do the rendering
|
||||||
item_xhtml = renderer(item)
|
item_xhtml = renderer(item)
|
||||||
@@ -1294,7 +1294,7 @@ class CodeResponse(LoncapaResponse):
|
|||||||
|
|
||||||
# State associated with the queueing request
|
# State associated with the queueing request
|
||||||
queuestate = {'key': queuekey,
|
queuestate = {'key': queuekey,
|
||||||
'time': qtime,}
|
'time': qtime, }
|
||||||
|
|
||||||
cmap = CorrectMap()
|
cmap = CorrectMap()
|
||||||
if error:
|
if error:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import xml.sax.saxutils as saxutils
|
|||||||
|
|
||||||
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
|
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def tst_render_template(template, context):
|
def tst_render_template(template, context):
|
||||||
"""
|
"""
|
||||||
A test version of render to template. Renders to the repr of the context, completely ignoring
|
A test version of render to template. Renders to the repr of the context, completely ignoring
|
||||||
@@ -25,7 +26,7 @@ test_system = Mock(
|
|||||||
user=Mock(),
|
user=Mock(),
|
||||||
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
|
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
|
||||||
debug=True,
|
debug=True,
|
||||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||||
anonymous_student_id = 'student'
|
anonymous_student_id='student'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from capa import customrender
|
|||||||
# just a handy shortcut
|
# just a handy shortcut
|
||||||
lookup_tag = customrender.registry.get_class_for_tag
|
lookup_tag = customrender.registry.get_class_for_tag
|
||||||
|
|
||||||
|
|
||||||
def extract_context(xml):
|
def extract_context(xml):
|
||||||
"""
|
"""
|
||||||
Given an xml element corresponding to the output of test_system.render_template, get back the
|
Given an xml element corresponding to the output of test_system.render_template, get back the
|
||||||
@@ -15,9 +16,11 @@ def extract_context(xml):
|
|||||||
"""
|
"""
|
||||||
return eval(xml.text)
|
return eval(xml.text)
|
||||||
|
|
||||||
|
|
||||||
def quote_attr(s):
|
def quote_attr(s):
|
||||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||||
|
|
||||||
|
|
||||||
class HelperTest(unittest.TestCase):
|
class HelperTest(unittest.TestCase):
|
||||||
'''
|
'''
|
||||||
Make sure that our helper function works!
|
Make sure that our helper function works!
|
||||||
@@ -50,7 +53,7 @@ class SolutionRenderTest(unittest.TestCase):
|
|||||||
# our test_system "renders" templates to a div with the repr of the context
|
# our test_system "renders" templates to a div with the repr of the context
|
||||||
xml = renderer.get_html()
|
xml = renderer.get_html()
|
||||||
context = extract_context(xml)
|
context = extract_context(xml)
|
||||||
self.assertEqual(context, {'id' : 'solution_12'})
|
self.assertEqual(context, {'id': 'solution_12'})
|
||||||
|
|
||||||
|
|
||||||
class MathRenderTest(unittest.TestCase):
|
class MathRenderTest(unittest.TestCase):
|
||||||
@@ -65,12 +68,11 @@ class MathRenderTest(unittest.TestCase):
|
|||||||
renderer = lookup_tag('math')(test_system, element)
|
renderer = lookup_tag('math')(test_system, element)
|
||||||
|
|
||||||
self.assertEqual(renderer.mathstr, mathjax_out)
|
self.assertEqual(renderer.mathstr, mathjax_out)
|
||||||
|
|
||||||
def test_parsing(self):
|
def test_parsing(self):
|
||||||
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
|
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
|
||||||
self.check_parse('$abc', '$abc')
|
self.check_parse('$abc', '$abc')
|
||||||
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
|
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
|
||||||
|
|
||||||
|
|
||||||
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
|
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ lookup_tag = inputtypes.registry.get_class_for_tag
|
|||||||
def quote_attr(s):
|
def quote_attr(s):
|
||||||
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
|
||||||
|
|
||||||
|
|
||||||
class OptionInputTest(unittest.TestCase):
|
class OptionInputTest(unittest.TestCase):
|
||||||
'''
|
'''
|
||||||
Make sure option inputs work
|
Make sure option inputs work
|
||||||
@@ -100,7 +101,7 @@ class ChoiceGroupTest(unittest.TestCase):
|
|||||||
'input_type': expected_input_type,
|
'input_type': expected_input_type,
|
||||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||||
('foil2', '<text>This is foil Two.</text>'),
|
('foil2', '<text>This is foil Two.</text>'),
|
||||||
('foil3', 'This is foil Three.'),],
|
('foil3', 'This is foil Three.'), ],
|
||||||
'name_array_suffix': expected_suffix, # what is this for??
|
'name_array_suffix': expected_suffix, # what is this for??
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ class JavascriptInputTest(unittest.TestCase):
|
|||||||
|
|
||||||
element = etree.fromstring(xml_str)
|
element = etree.fromstring(xml_str)
|
||||||
|
|
||||||
state = {'value': '3',}
|
state = {'value': '3', }
|
||||||
the_input = lookup_tag('javascriptinput')(test_system, element, state)
|
the_input = lookup_tag('javascriptinput')(test_system, element, state)
|
||||||
|
|
||||||
context = the_input._get_render_context()
|
context = the_input._get_render_context()
|
||||||
@@ -149,7 +150,7 @@ class JavascriptInputTest(unittest.TestCase):
|
|||||||
'params': params,
|
'params': params,
|
||||||
'display_file': display_file,
|
'display_file': display_file,
|
||||||
'display_class': display_class,
|
'display_class': display_class,
|
||||||
'problem_state': problem_state,}
|
'problem_state': problem_state, }
|
||||||
|
|
||||||
self.assertEqual(context, expected)
|
self.assertEqual(context, expected)
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ class TextLineTest(unittest.TestCase):
|
|||||||
|
|
||||||
element = etree.fromstring(xml_str)
|
element = etree.fromstring(xml_str)
|
||||||
|
|
||||||
state = {'value': 'BumbleBee',}
|
state = {'value': 'BumbleBee', }
|
||||||
the_input = lookup_tag('textline')(test_system, element, state)
|
the_input = lookup_tag('textline')(test_system, element, state)
|
||||||
|
|
||||||
context = the_input._get_render_context()
|
context = the_input._get_render_context()
|
||||||
@@ -193,7 +194,7 @@ class TextLineTest(unittest.TestCase):
|
|||||||
|
|
||||||
element = etree.fromstring(xml_str)
|
element = etree.fromstring(xml_str)
|
||||||
|
|
||||||
state = {'value': 'BumbleBee',}
|
state = {'value': 'BumbleBee', }
|
||||||
the_input = lookup_tag('textline')(test_system, element, state)
|
the_input = lookup_tag('textline')(test_system, element, state)
|
||||||
|
|
||||||
context = the_input._get_render_context()
|
context = the_input._get_render_context()
|
||||||
@@ -231,7 +232,7 @@ class FileSubmissionTest(unittest.TestCase):
|
|||||||
|
|
||||||
state = {'value': 'BumbleBee.py',
|
state = {'value': 'BumbleBee.py',
|
||||||
'status': 'incomplete',
|
'status': 'incomplete',
|
||||||
'feedback' : {'message': '3'}, }
|
'feedback': {'message': '3'}, }
|
||||||
input_class = lookup_tag('filesubmission')
|
input_class = lookup_tag('filesubmission')
|
||||||
the_input = input_class(test_system, element, state)
|
the_input = input_class(test_system, element, state)
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ class CodeInputTest(unittest.TestCase):
|
|||||||
|
|
||||||
state = {'value': 'print "good evening"',
|
state = {'value': 'print "good evening"',
|
||||||
'status': 'incomplete',
|
'status': 'incomplete',
|
||||||
'feedback' : {'message': '3'}, }
|
'feedback': {'message': '3'}, }
|
||||||
|
|
||||||
input_class = lookup_tag('codeinput')
|
input_class = lookup_tag('codeinput')
|
||||||
the_input = input_class(test_system, element, state)
|
the_input = input_class(test_system, element, state)
|
||||||
@@ -488,7 +489,7 @@ class ChemicalEquationTest(unittest.TestCase):
|
|||||||
|
|
||||||
element = etree.fromstring(xml_str)
|
element = etree.fromstring(xml_str)
|
||||||
|
|
||||||
state = {'value': 'H2OYeah',}
|
state = {'value': 'H2OYeah', }
|
||||||
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||||
|
|
||||||
context = the_input._get_render_context()
|
context = the_input._get_render_context()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from capa.correctmap import CorrectMap
|
|||||||
from capa.util import convert_files_to_filenames
|
from capa.util import convert_files_to_filenames
|
||||||
from capa.xqueue_interface import dateformat
|
from capa.xqueue_interface import dateformat
|
||||||
|
|
||||||
|
|
||||||
class MultiChoiceTest(unittest.TestCase):
|
class MultiChoiceTest(unittest.TestCase):
|
||||||
def test_MC_grade(self):
|
def test_MC_grade(self):
|
||||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
||||||
@@ -295,16 +296,16 @@ class CodeResponseTest(unittest.TestCase):
|
|||||||
old_cmap = CorrectMap()
|
old_cmap = CorrectMap()
|
||||||
for i, answer_id in enumerate(answer_ids):
|
for i, answer_id in enumerate(answer_ids):
|
||||||
queuekey = 1000 + i
|
queuekey = 1000 + i
|
||||||
queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now())
|
queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
|
||||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
|
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
|
||||||
|
|
||||||
# Message format common to external graders
|
# Message format common to external graders
|
||||||
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
grader_msg = '<span>MESSAGE</span>' # Must be valid XML
|
||||||
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg': grader_msg})
|
correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
|
||||||
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg': grader_msg})
|
incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
|
||||||
|
|
||||||
xserver_msgs = {'correct': correct_score_msg,
|
xserver_msgs = {'correct': correct_score_msg,
|
||||||
'incorrect': incorrect_score_msg,}
|
'incorrect': incorrect_score_msg, }
|
||||||
|
|
||||||
# Incorrect queuekey, state should not be updated
|
# Incorrect queuekey, state should not be updated
|
||||||
for correctness in ['correct', 'incorrect']:
|
for correctness in ['correct', 'incorrect']:
|
||||||
@@ -325,7 +326,7 @@ class CodeResponseTest(unittest.TestCase):
|
|||||||
|
|
||||||
new_cmap = CorrectMap()
|
new_cmap = CorrectMap()
|
||||||
new_cmap.update(old_cmap)
|
new_cmap.update(old_cmap)
|
||||||
npoints = 1 if correctness=='correct' else 0
|
npoints = 1 if correctness == 'correct' else 0
|
||||||
new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None)
|
new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None)
|
||||||
|
|
||||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
||||||
@@ -361,7 +362,7 @@ class CodeResponseTest(unittest.TestCase):
|
|||||||
for i, answer_id in enumerate(answer_ids):
|
for i, answer_id in enumerate(answer_ids):
|
||||||
queuekey = 1000 + i
|
queuekey = 1000 + i
|
||||||
latest_timestamp = datetime.now()
|
latest_timestamp = datetime.now()
|
||||||
queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp)
|
queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
|
||||||
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
|
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
|
||||||
test_lcp.correct_map.update(cmap)
|
test_lcp.correct_map.update(cmap)
|
||||||
|
|
||||||
@@ -412,6 +413,7 @@ class ChoiceResponseTest(unittest.TestCase):
|
|||||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
|
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
|
||||||
|
|
||||||
|
|
||||||
class JavascriptResponseTest(unittest.TestCase):
|
class JavascriptResponseTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_jr_grade(self):
|
def test_jr_grade(self):
|
||||||
@@ -424,4 +426,3 @@ class JavascriptResponseTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
|
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
|
||||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,17 @@ def convert_files_to_filenames(answers):
|
|||||||
new_answers = dict()
|
new_answers = dict()
|
||||||
for answer_id in answers.keys():
|
for answer_id in answers.keys():
|
||||||
answer = answers[answer_id]
|
answer = answers[answer_id]
|
||||||
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
if is_list_of_files(answer): # Files are stored as a list, even if one file
|
||||||
new_answers[answer_id] = [f.name for f in answer]
|
new_answers[answer_id] = [f.name for f in answer]
|
||||||
else:
|
else:
|
||||||
new_answers[answer_id] = answers[answer_id]
|
new_answers[answer_id] = answers[answer_id]
|
||||||
return new_answers
|
return new_answers
|
||||||
|
|
||||||
|
|
||||||
def is_list_of_files(files):
|
def is_list_of_files(files):
|
||||||
return isinstance(files, list) and all(is_file(f) for f in files)
|
return isinstance(files, list) and all(is_file(f) for f in files)
|
||||||
|
|
||||||
|
|
||||||
def is_file(file_to_test):
|
def is_file(file_to_test):
|
||||||
'''
|
'''
|
||||||
Duck typing to check if 'file_to_test' is a File object
|
Duck typing to check if 'file_to_test' is a File object
|
||||||
@@ -79,11 +81,10 @@ def find_with_default(node, path, default):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
node.find(path).text if the find succeeds, default otherwise.
|
node.find(path).text if the find succeeds, default otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
v = node.find(path)
|
v = node.find(path)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
return v.text
|
return v.text
|
||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import requests
|
|||||||
log = logging.getLogger('mitx.' + __name__)
|
log = logging.getLogger('mitx.' + __name__)
|
||||||
dateformat = '%Y%m%d%H%M%S'
|
dateformat = '%Y%m%d%H%M%S'
|
||||||
|
|
||||||
|
|
||||||
def make_hashkey(seed):
|
def make_hashkey(seed):
|
||||||
'''
|
'''
|
||||||
Generate a string key by hashing
|
Generate a string key by hashing
|
||||||
@@ -29,9 +30,9 @@ def make_xheader(lms_callback_url, lms_key, queue_name):
|
|||||||
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
|
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
return json.dumps({'lms_callback_url': lms_callback_url,
|
||||||
'lms_key': lms_key,
|
'lms_key': lms_key,
|
||||||
'queue_name': queue_name })
|
'queue_name': queue_name})
|
||||||
|
|
||||||
|
|
||||||
def parse_xreply(xreply):
|
def parse_xreply(xreply):
|
||||||
@@ -96,18 +97,18 @@ class XQueueInterface(object):
|
|||||||
|
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
payload = { 'username': self.auth['username'],
|
payload = {'username': self.auth['username'],
|
||||||
'password': self.auth['password'] }
|
'password': self.auth['password']}
|
||||||
return self._http_post(self.url + '/xqueue/login/', payload)
|
return self._http_post(self.url + '/xqueue/login/', payload)
|
||||||
|
|
||||||
|
|
||||||
def _send_to_queue(self, header, body, files_to_upload):
|
def _send_to_queue(self, header, body, files_to_upload):
|
||||||
payload = {'xqueue_header': header,
|
payload = {'xqueue_header': header,
|
||||||
'xqueue_body' : body}
|
'xqueue_body': body}
|
||||||
files = {}
|
files = {}
|
||||||
if files_to_upload is not None:
|
if files_to_upload is not None:
|
||||||
for f in files_to_upload:
|
for f in files_to_upload:
|
||||||
files.update({ f.name: f })
|
files.update({f.name: f})
|
||||||
|
|
||||||
return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
|
return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ A handy util to print a django-debug-screen-like stack trace with
|
|||||||
values of local variables.
|
values of local variables.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys, traceback
|
import sys
|
||||||
|
import traceback
|
||||||
from django.utils.encoding import smart_unicode
|
from django.utils.encoding import smart_unicode
|
||||||
|
|
||||||
|
|
||||||
@@ -48,5 +49,3 @@ def supertrace(max_len=160):
|
|||||||
print s
|
print s
|
||||||
except:
|
except:
|
||||||
print "<ERROR WHILE PRINTING VALUE>"
|
print "<ERROR WHILE PRINTING VALUE>"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ setup(
|
|||||||
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
|
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
|
||||||
"problem = xmodule.capa_module:CapaDescriptor",
|
"problem = xmodule.capa_module:CapaDescriptor",
|
||||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||||
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ABTestModule(XModule):
|
|||||||
|
|
||||||
def get_shared_state(self):
|
def get_shared_state(self):
|
||||||
return json.dumps({'group': self.group})
|
return json.dumps({'group': self.group})
|
||||||
|
|
||||||
def get_child_descriptors(self):
|
def get_child_descriptors(self):
|
||||||
active_locations = set(self.definition['data']['group_content'][self.group])
|
active_locations = set(self.definition['data']['group_content'][self.group])
|
||||||
return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
|
return [desc for desc in self.descriptor.get_children() if desc.location.url() in active_locations]
|
||||||
@@ -171,7 +171,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
|
|||||||
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||||
|
|
||||||
return xml_object
|
return xml_object
|
||||||
|
|
||||||
|
|
||||||
def has_dynamic_children(self):
|
def has_dynamic_children(self):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
|
|||||||
# Generated this many different variants of problems with rerandomize=per_student
|
# Generated this many different variants of problems with rerandomize=per_student
|
||||||
NUM_RANDOMIZATION_BINS = 20
|
NUM_RANDOMIZATION_BINS = 20
|
||||||
|
|
||||||
|
|
||||||
def randomization_bin(seed, problem_id):
|
def randomization_bin(seed, problem_id):
|
||||||
"""
|
"""
|
||||||
Pick a randomization bin for the problem given the user's seed and a problem id.
|
Pick a randomization bin for the problem given the user's seed and a problem id.
|
||||||
@@ -43,6 +44,7 @@ def randomization_bin(seed, problem_id):
|
|||||||
# get the first few digits of the hash, convert to an int, then mod.
|
# get the first few digits of the hash, convert to an int, then mod.
|
||||||
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
|
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
|
||||||
|
|
||||||
|
|
||||||
def only_one(lst, default="", process=lambda x: x):
|
def only_one(lst, default="", process=lambda x: x):
|
||||||
"""
|
"""
|
||||||
If lst is empty, returns default
|
If lst is empty, returns default
|
||||||
@@ -283,7 +285,7 @@ class CapaModule(XModule):
|
|||||||
|
|
||||||
# Next, generate a fresh LoncapaProblem
|
# Next, generate a fresh LoncapaProblem
|
||||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||||
state=None, # Tabula rasa
|
state=None, # Tabula rasa
|
||||||
seed=self.seed, system=self.system)
|
seed=self.seed, system=self.system)
|
||||||
|
|
||||||
# Prepend a scary warning to the student
|
# Prepend a scary warning to the student
|
||||||
@@ -302,7 +304,7 @@ class CapaModule(XModule):
|
|||||||
html = warning
|
html = warning
|
||||||
try:
|
try:
|
||||||
html += self.lcp.get_html()
|
html += self.lcp.get_html()
|
||||||
except Exception, err: # Couldn't do it. Give up
|
except Exception, err: # Couldn't do it. Give up
|
||||||
log.exception(err)
|
log.exception(err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -315,7 +317,7 @@ class CapaModule(XModule):
|
|||||||
# check button is context-specific.
|
# check button is context-specific.
|
||||||
|
|
||||||
# Put a "Check" button if unlimited attempts or still some left
|
# Put a "Check" button if unlimited attempts or still some left
|
||||||
if self.max_attempts is None or self.attempts < self.max_attempts-1:
|
if self.max_attempts is None or self.attempts < self.max_attempts - 1:
|
||||||
check_button = "Check"
|
check_button = "Check"
|
||||||
else:
|
else:
|
||||||
# Will be final check so let user know that
|
# Will be final check so let user know that
|
||||||
@@ -561,9 +563,9 @@ class CapaModule(XModule):
|
|||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||||
waittime_between_requests = self.system.xqueue['waittime']
|
waittime_between_requests = self.system.xqueue['waittime']
|
||||||
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
|
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
|
||||||
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
||||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
old_state = self.lcp.get_state()
|
old_state = self.lcp.get_state()
|
||||||
@@ -596,7 +598,7 @@ class CapaModule(XModule):
|
|||||||
event_info['attempts'] = self.attempts
|
event_info['attempts'] = self.attempts
|
||||||
self.system.track_function('save_problem_check', event_info)
|
self.system.track_function('save_problem_check', event_info)
|
||||||
|
|
||||||
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
|
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||||
self.system.psychometrics_handler(self.get_instance_state())
|
self.system.psychometrics_handler(self.get_instance_state())
|
||||||
|
|
||||||
# render problem into HTML
|
# render problem into HTML
|
||||||
@@ -707,7 +709,7 @@ class CapaDescriptor(RawDescriptor):
|
|||||||
@property
|
@property
|
||||||
def editable_metadata_fields(self):
|
def editable_metadata_fields(self):
|
||||||
"""Remove metadata from the editable fields since it has its own editor"""
|
"""Remove metadata from the editable fields since it has its own editor"""
|
||||||
subset = super(CapaDescriptor,self).editable_metadata_fields
|
subset = super(CapaDescriptor, self).editable_metadata_fields
|
||||||
if 'markdown' in subset:
|
if 'markdown' in subset:
|
||||||
subset.remove('markdown')
|
subset.remove('markdown')
|
||||||
return subset
|
return subset
|
||||||
|
|||||||
@@ -50,14 +50,16 @@ ACCEPT_FILE_UPLOAD = False
|
|||||||
TRUE_DICT = ["True", True, "TRUE", "true"]
|
TRUE_DICT = ["True", True, "TRUE", "true"]
|
||||||
|
|
||||||
HUMAN_TASK_TYPE = {
|
HUMAN_TASK_TYPE = {
|
||||||
'selfassessment' : "Self Assessment",
|
'selfassessment': "Self Assessment",
|
||||||
'openended' : "External Grader",
|
'openended': "External Grader",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IncorrectMaxScoreError(Exception):
|
class IncorrectMaxScoreError(Exception):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
class CombinedOpenEndedModule(XModule):
|
class CombinedOpenEndedModule(XModule):
|
||||||
"""
|
"""
|
||||||
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
|
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
|
||||||
@@ -700,4 +702,4 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
|||||||
for child in ['task']:
|
for child in ['task']:
|
||||||
add_child(child)
|
add_child(child)
|
||||||
|
|
||||||
return elt
|
return elt
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
log=logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RubricParsingError(Exception):
|
class RubricParsingError(Exception):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
class CombinedOpenEndedRubric(object):
|
class CombinedOpenEndedRubric(object):
|
||||||
|
|
||||||
def __init__ (self, system, view_only = False):
|
def __init__ (self, system, view_only = False):
|
||||||
@@ -27,8 +29,8 @@ class CombinedOpenEndedRubric(object):
|
|||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
rubric_categories = self.extract_categories(rubric_xml)
|
rubric_categories = self.extract_categories(rubric_xml)
|
||||||
html = self.system.render_template('open_ended_rubric.html',
|
html = self.system.render_template('open_ended_rubric.html',
|
||||||
{'categories' : rubric_categories,
|
{'categories': rubric_categories,
|
||||||
'has_score': self.has_score,
|
'has_score': self.has_score,
|
||||||
'view_only': self.view_only})
|
'view_only': self.view_only})
|
||||||
success = True
|
success = True
|
||||||
@@ -60,8 +62,8 @@ class CombinedOpenEndedRubric(object):
|
|||||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||||
},
|
},
|
||||||
{ category: "Category 2 Name",
|
{ category: "Category 2 Name",
|
||||||
options: [{text: "Option 1 Name", points: 0},
|
options: [{text: "Option 1 Name", points: 0},
|
||||||
{text: "Option 2 Name", points: 1},
|
{text: "Option 2 Name", points: 1},
|
||||||
{text: "Option 3 Name", points: 2]}]
|
{text: "Option 3 Name", points: 2]}]
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -77,7 +79,7 @@ class CombinedOpenEndedRubric(object):
|
|||||||
|
|
||||||
|
|
||||||
def extract_category(self, category):
|
def extract_category(self, category):
|
||||||
'''
|
'''
|
||||||
construct an individual category
|
construct an individual category
|
||||||
{category: "Category 1 Name",
|
{category: "Category 1 Name",
|
||||||
options: [{text: "Option 1 text", points: 1},
|
options: [{text: "Option 1 text", points: 1},
|
||||||
@@ -110,7 +112,7 @@ class CombinedOpenEndedRubric(object):
|
|||||||
autonumbering = True
|
autonumbering = True
|
||||||
# parse options
|
# parse options
|
||||||
for option in optionsxml:
|
for option in optionsxml:
|
||||||
if option.tag != 'option':
|
if option.tag != 'option':
|
||||||
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||||
else:
|
else:
|
||||||
pointstr = option.get("points")
|
pointstr = option.get("points")
|
||||||
@@ -127,7 +129,7 @@ class CombinedOpenEndedRubric(object):
|
|||||||
cur_points = cur_points + 1
|
cur_points = cur_points + 1
|
||||||
else:
|
else:
|
||||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
|
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
|
||||||
|
|
||||||
selected = score == points
|
selected = score == points
|
||||||
optiontext = option.text
|
optiontext = option.text
|
||||||
options.append({'text': option.text, 'points': points, 'selected': selected})
|
options.append({'text': option.text, 'points': points, 'selected': selected})
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ from pkg_resources import resource_string
|
|||||||
|
|
||||||
log = logging.getLogger('mitx.' + __name__)
|
log = logging.getLogger('mitx.' + __name__)
|
||||||
|
|
||||||
|
|
||||||
class ConditionalModule(XModule):
|
class ConditionalModule(XModule):
|
||||||
'''
|
'''
|
||||||
Blocks child module from showing unless certain conditions are met.
|
Blocks child module from showing unless certain conditions are met.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
|
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
|
||||||
<video url_name="secret_video" />
|
<video url_name="secret_video" />
|
||||||
</conditional>
|
</conditional>
|
||||||
@@ -37,13 +38,13 @@ class ConditionalModule(XModule):
|
|||||||
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
|
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
In addition to the normal XModule init, provide:
|
In addition to the normal XModule init, provide:
|
||||||
|
|
||||||
self.condition = string describing condition required
|
self.condition = string describing condition required
|
||||||
|
|
||||||
"""
|
"""
|
||||||
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
|
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
|
||||||
self.contents = None
|
self.contents = None
|
||||||
self.condition = self.metadata.get('condition','')
|
self.condition = self.metadata.get('condition', '')
|
||||||
#log.debug('conditional module required=%s' % self.required_modules_list)
|
#log.debug('conditional module required=%s' % self.required_modules_list)
|
||||||
|
|
||||||
def _get_required_modules(self):
|
def _get_required_modules(self):
|
||||||
@@ -56,7 +57,7 @@ class ConditionalModule(XModule):
|
|||||||
def is_condition_satisfied(self):
|
def is_condition_satisfied(self):
|
||||||
self._get_required_modules()
|
self._get_required_modules()
|
||||||
|
|
||||||
if self.condition=='require_completed':
|
if self.condition == 'require_completed':
|
||||||
# all required modules must be completed, as determined by
|
# all required modules must be completed, as determined by
|
||||||
# the modules .is_completed() method
|
# the modules .is_completed() method
|
||||||
for module in self.required_modules:
|
for module in self.required_modules:
|
||||||
@@ -70,7 +71,7 @@ class ConditionalModule(XModule):
|
|||||||
else:
|
else:
|
||||||
log.debug('conditional module: %s IS completed' % module)
|
log.debug('conditional module: %s IS completed' % module)
|
||||||
return True
|
return True
|
||||||
elif self.condition=='require_attempted':
|
elif self.condition == 'require_attempted':
|
||||||
# all required modules must be attempted, as determined by
|
# all required modules must be attempted, as determined by
|
||||||
# the modules .is_attempted() method
|
# the modules .is_attempted() method
|
||||||
for module in self.required_modules:
|
for module in self.required_modules:
|
||||||
@@ -111,9 +112,10 @@ class ConditionalModule(XModule):
|
|||||||
|
|
||||||
# for now, just deal with one child
|
# for now, just deal with one child
|
||||||
html = self.contents[0]
|
html = self.contents[0]
|
||||||
|
|
||||||
return json.dumps({'html': html})
|
return json.dumps({'html': html})
|
||||||
|
|
||||||
|
|
||||||
class ConditionalDescriptor(SequenceDescriptor):
|
class ConditionalDescriptor(SequenceDescriptor):
|
||||||
module_class = ConditionalModule
|
module_class = ConditionalModule
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ class ConditionalDescriptor(SequenceDescriptor):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
|
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')]
|
required_module_list = [tuple(x.split('/', 1)) for x in self.metadata.get('required', '').split('&')]
|
||||||
self.required_module_locations = []
|
self.required_module_locations = []
|
||||||
for (tag, name) in required_module_list:
|
for (tag, name) in required_module_list:
|
||||||
loc = self.location.dict()
|
loc = self.location.dict()
|
||||||
@@ -133,9 +135,8 @@ class ConditionalDescriptor(SequenceDescriptor):
|
|||||||
loc['name'] = name
|
loc['name'] = name
|
||||||
self.required_module_locations.append(Location(loc))
|
self.required_module_locations.append(Location(loc))
|
||||||
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
|
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
|
||||||
|
|
||||||
def get_required_module_descriptors(self):
|
def get_required_module_descriptors(self):
|
||||||
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
|
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
|
||||||
not children of this module"""
|
not children of this module"""
|
||||||
return [self.system.load_item(loc) for loc in self.required_module_locations]
|
return [self.system.load_item(loc) for loc in self.required_module_locations]
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ from xmodule.modulestore import Location
|
|||||||
from .django import contentstore
|
from .django import contentstore
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
class StaticContent(object):
|
class StaticContent(object):
|
||||||
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None):
|
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None):
|
||||||
self.location = loc
|
self.location = loc
|
||||||
self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed
|
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.data = data
|
self.data = data
|
||||||
self.last_modified_at = last_modified_at
|
self.last_modified_at = last_modified_at
|
||||||
self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None
|
self.thumbnail_location = Location(thumbnail_location) if thumbnail_location is not None else None
|
||||||
# optional information about where this file was imported from. This is needed to support import/export
|
# optional information about where this file was imported from. This is needed to support import/export
|
||||||
# cycles
|
# cycles
|
||||||
self.import_path = import_path
|
self.import_path = import_path
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class StaticContent(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_thumbnail_name(original_name):
|
def generate_thumbnail_name(original_name):
|
||||||
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
return ('{0}' + XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(original_name)[0])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_location(org, course, name, revision=None, is_thumbnail=False):
|
def compute_location(org, course, name, revision=None, is_thumbnail=False):
|
||||||
@@ -41,7 +42,7 @@ class StaticContent(object):
|
|||||||
|
|
||||||
def get_url_path(self):
|
def get_url_path(self):
|
||||||
return StaticContent.get_url_path_from_location(self.location)
|
return StaticContent.get_url_path_from_location(self.location)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_url_path_from_location(location):
|
def get_url_path_from_location(location):
|
||||||
if location is not None:
|
if location is not None:
|
||||||
@@ -56,15 +57,15 @@ class StaticContent(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_id_from_location(location):
|
def get_id_from_location(location):
|
||||||
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
|
return {'tag': location.tag, 'org': location.org, 'course': location.course,
|
||||||
'category' : location.category, 'name' : location.name,
|
'category': location.category, 'name': location.name,
|
||||||
'revision' : location.revision}
|
'revision': location.revision}
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_location_from_path(path):
|
def get_location_from_path(path):
|
||||||
# remove leading / character if it is there one
|
# remove leading / character if it is there one
|
||||||
if path.startswith('/'):
|
if path.startswith('/'):
|
||||||
path = path[1:]
|
path = path[1:]
|
||||||
|
|
||||||
return Location(path.split('/'))
|
return Location(path.split('/'))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -77,7 +78,7 @@ class StaticContent(object):
|
|||||||
return StaticContent.get_url_path_from_location(loc)
|
return StaticContent.get_url_path_from_location(loc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ContentStore(object):
|
class ContentStore(object):
|
||||||
'''
|
'''
|
||||||
@@ -95,14 +96,14 @@ class ContentStore(object):
|
|||||||
|
|
||||||
[
|
[
|
||||||
|
|
||||||
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
|
||||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
|
||||||
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||||
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
|
||||||
|
|
||||||
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
|
||||||
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
|
||||||
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
|
||||||
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
|
||||||
|
|
||||||
....
|
....
|
||||||
@@ -117,7 +118,7 @@ class ContentStore(object):
|
|||||||
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
|
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
|
||||||
|
|
||||||
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
|
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
|
||||||
thumbnail_name, is_thumbnail = True)
|
thumbnail_name, is_thumbnail=True)
|
||||||
|
|
||||||
# if we're uploading an image, then let's generate a thumbnail so that we can
|
# if we're uploading an image, then let's generate a thumbnail so that we can
|
||||||
# serve it up when needed without having to rescale on the fly
|
# serve it up when needed without having to rescale on the fly
|
||||||
@@ -129,7 +130,7 @@ class ContentStore(object):
|
|||||||
# @todo: move the thumbnail size to a configuration setting?!?
|
# @todo: move the thumbnail size to a configuration setting?!?
|
||||||
im = Image.open(StringIO.StringIO(content.data))
|
im = Image.open(StringIO.StringIO(content.data))
|
||||||
|
|
||||||
# I've seen some exceptions from the PIL library when trying to save palletted
|
# I've seen some exceptions from the PIL library when trying to save palletted
|
||||||
# PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
|
# PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
|
||||||
im = im.convert('RGB')
|
im = im.convert('RGB')
|
||||||
size = 128, 128
|
size = 128, 128
|
||||||
@@ -139,7 +140,7 @@ class ContentStore(object):
|
|||||||
thumbnail_file.seek(0)
|
thumbnail_file.seek(0)
|
||||||
|
|
||||||
# store this thumbnail as any other piece of content
|
# store this thumbnail as any other piece of content
|
||||||
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
|
||||||
'image/jpeg', thumbnail_file)
|
'image/jpeg', thumbnail_file)
|
||||||
|
|
||||||
contentstore().save(thumbnail_content)
|
contentstore().save(thumbnail_content)
|
||||||
@@ -149,7 +150,3 @@ class ContentStore(object):
|
|||||||
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
|
logging.exception("Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e)))
|
||||||
|
|
||||||
return thumbnail_content, thumbnail_file_location
|
return thumbnail_content, thumbnail_file_location
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user