Merge branch 'master' into feature/kevin/pinning_inline
@@ -73,6 +73,9 @@ class Command(BaseCommand):
|
||||
ended_courses.append(course_id)
|
||||
|
||||
for course_id in ended_courses:
|
||||
# prefetch all chapters/sequentials by saying depth=2
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
|
||||
|
||||
print "Fetching enrolled students for {0}".format(course_id)
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
@@ -99,6 +102,6 @@ class Command(BaseCommand):
|
||||
student, course_id)['status'] in valid_statuses:
|
||||
if not options['noop']:
|
||||
# Add the certificate request to the queue
|
||||
ret = xq.add_cert(student, course_id)
|
||||
ret = xq.add_cert(student, course_id, course=course)
|
||||
if ret == 'generating':
|
||||
print '{0} - {1}'.format(student, ret)
|
||||
|
||||
@@ -115,7 +115,7 @@ class XQueueCertInterface(object):
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def add_cert(self, student, course_id):
|
||||
def add_cert(self, student, course_id, course=None):
|
||||
"""
|
||||
|
||||
Arguments:
|
||||
@@ -151,9 +151,12 @@ class XQueueCertInterface(object):
|
||||
|
||||
if cert_status in VALID_STATUSES:
|
||||
# grade the student
|
||||
course = courses.get_course_by_id(course_id)
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
|
||||
# re-use the course passed in optionally so we don't have to re-fetch everything
|
||||
# for every student
|
||||
if course is None:
|
||||
course = courses.get_course_by_id(course_id)
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(
|
||||
user=student, course_id=course_id)
|
||||
|
||||
@@ -3,13 +3,11 @@ from django.test.utils import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
|
||||
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class WikiRedirectTestCase(PageLoader):
|
||||
class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader):
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
|
||||
|
||||
def test_wiki_redirect(self):
|
||||
"""
|
||||
Test that requesting wiki URLs redirect properly to or out of classes.
|
||||
@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader):
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp['Location'], 'http://testserver' + destination)
|
||||
|
||||
|
||||
def create_course_page(self, course):
|
||||
"""
|
||||
Test that loading the course wiki page creates the wiki page.
|
||||
@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader):
|
||||
self.assertTrue("course info" in resp.content.lower())
|
||||
self.assertTrue("courseware" in resp.content.lower())
|
||||
|
||||
|
||||
def test_course_navigator(self):
|
||||
""""
|
||||
Test that going from a course page to a wiki page contains the course navigator.
|
||||
@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader):
|
||||
self.enroll(self.toy)
|
||||
self.create_course_page(self.toy)
|
||||
|
||||
|
||||
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
|
||||
referer = reverse("courseware", kwargs={'course_id': self.toy.id})
|
||||
|
||||
|
||||
@@ -1,90 +1,21 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from django.core.management import call_command
|
||||
from nose.tools import assert_equals, assert_in
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from terrain.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
import time
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule import seq_module, vertical_module
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@step(u'I wait (?:for )?"(\d+)" seconds?$')
|
||||
def wait(step, seconds):
|
||||
time.sleep(float(seconds))
|
||||
|
||||
|
||||
@step('I (?:visit|access|open) the homepage$')
|
||||
def i_visit_the_homepage(step):
|
||||
world.browser.visit(django_url('/'))
|
||||
assert world.browser.is_element_present_by_css('header.global', 10)
|
||||
|
||||
|
||||
@step(u'I (?:visit|access|open) the dashboard$')
|
||||
def i_visit_the_dashboard(step):
|
||||
world.browser.visit(django_url('/dashboard'))
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
|
||||
|
||||
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
|
||||
def click_the_link_called(step, text):
|
||||
world.browser.find_link_by_text(text).click()
|
||||
|
||||
|
||||
@step('I should be on the dashboard page$')
|
||||
def i_should_be_on_the_dashboard(step):
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
assert world.browser.title == 'Dashboard'
|
||||
|
||||
|
||||
@step(u'I (?:visit|access|open) the courses page$')
|
||||
def i_am_on_the_courses_page(step):
|
||||
world.browser.visit(django_url('/courses'))
|
||||
assert world.browser.is_element_present_by_css('section.courses')
|
||||
|
||||
|
||||
@step('I should see that the path is "([^"]*)"$')
|
||||
def i_should_see_that_the_path_is(step, path):
|
||||
assert world.browser.url == django_url(path)
|
||||
|
||||
|
||||
@step(u'the page title should be "([^"]*)"$')
|
||||
def the_page_title_should_be(step, title):
|
||||
assert world.browser.title == title
|
||||
|
||||
|
||||
@step(r'should see that the url is "([^"]*)"$')
|
||||
def should_have_the_url(step, url):
|
||||
assert_equals(world.browser.url, url)
|
||||
|
||||
|
||||
@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
|
||||
def should_see_a_link_called(step, text):
|
||||
assert len(world.browser.find_link_by_text(text)) > 0
|
||||
|
||||
|
||||
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
|
||||
def should_see_in_the_page(step, text):
|
||||
assert_in(text, world.browser.html)
|
||||
|
||||
|
||||
@step('I am logged in$')
|
||||
def i_am_logged_in(step):
|
||||
world.create_user('robot')
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
|
||||
|
||||
@step('I am not logged in$')
|
||||
def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
|
||||
TEST_COURSE_ORG = 'edx'
|
||||
TEST_COURSE_NAME = 'Test Course'
|
||||
TEST_SECTION_NAME = "Problem"
|
||||
@@ -96,22 +27,22 @@ def create_course(step, course):
|
||||
# First clear the modulestore so we don't try to recreate
|
||||
# the same course twice
|
||||
# This also ensures that the necessary templates are loaded
|
||||
flush_xmodule_store()
|
||||
world.clear_courses()
|
||||
|
||||
# Create the course
|
||||
# We always use the same org and display name,
|
||||
# but vary the course identifier (e.g. 600x or 191x)
|
||||
course = CourseFactory.create(org=TEST_COURSE_ORG,
|
||||
number=course,
|
||||
display_name=TEST_COURSE_NAME)
|
||||
course = world.CourseFactory.create(org=TEST_COURSE_ORG,
|
||||
number=course,
|
||||
display_name=TEST_COURSE_NAME)
|
||||
|
||||
# Add a section to the course to contain problems
|
||||
section = ItemFactory.create(parent_location=course.location,
|
||||
display_name=TEST_SECTION_NAME)
|
||||
section = world.ItemFactory.create(parent_location=course.location,
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
problem_section = ItemFactory.create(parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name=TEST_SECTION_NAME)
|
||||
problem_section = world.ItemFactory.create(parent_location=section.location,
|
||||
template='i4x://edx/templates/sequential/Empty',
|
||||
display_name=TEST_SECTION_NAME)
|
||||
|
||||
|
||||
@step(u'I am registered for the course "([^"]*)"$')
|
||||
@@ -124,44 +55,22 @@ def i_am_registered_for_the_course(step, course):
|
||||
u = User.objects.get(username='robot')
|
||||
|
||||
# If the user is not already enrolled, enroll the user.
|
||||
# TODO: change to factory
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
|
||||
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
world.log_in('robot', 'test')
|
||||
|
||||
|
||||
@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
|
||||
def add_tab_to_course(step, course, extra_tab_name):
|
||||
section_item = ItemFactory.create(parent_location=course_location(course),
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name=str(extra_tab_name))
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
def i_am_an_edx_user(step):
|
||||
world.create_user('robot')
|
||||
|
||||
|
||||
@step(u'User "([^"]*)" is an edX user$')
|
||||
def registered_edx_user(step, uname):
|
||||
world.create_user(uname)
|
||||
|
||||
|
||||
def flush_xmodule_store():
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
_MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
update_templates()
|
||||
section_item = world.ItemFactory.create(parent_location=course_location(course),
|
||||
template="i4x://edx/templates/static_tab/Empty",
|
||||
display_name=str(extra_tab_name))
|
||||
|
||||
|
||||
def course_id(course_num):
|
||||
return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
|
||||
TEST_COURSE_NAME.replace(" ", "_"))
|
||||
TEST_COURSE_NAME.replace(" ", "_"))
|
||||
|
||||
|
||||
def course_location(course_num):
|
||||
@@ -178,3 +87,87 @@ def section_location(course_num):
|
||||
course=course_num,
|
||||
category='sequential',
|
||||
name=TEST_SECTION_NAME.replace(" ", "_"))
|
||||
|
||||
|
||||
def get_courses():
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
'''
|
||||
courses = [c for c in modulestore().get_courses()
|
||||
if isinstance(c, CourseDescriptor)]
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
return courses
|
||||
|
||||
|
||||
def get_courseware_with_tabs(course_id):
|
||||
"""
|
||||
Given a course_id (string), return a courseware array of dictionaries for the
|
||||
top three levels of navigation. Same as get_courseware() except include
|
||||
the tabs on the right hand main navigation page.
|
||||
|
||||
This hides the appropriate courseware as defined by the hide_from_toc field:
|
||||
chapter.lms.hide_from_toc
|
||||
|
||||
Example:
|
||||
|
||||
[{
|
||||
'chapter_name': 'Overview',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Welcome',
|
||||
'tab_classes': []
|
||||
}, {
|
||||
'clickable_tab_count': 1,
|
||||
'section_name': 'System Usage Sequence',
|
||||
'tab_classes': ['VerticalDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Lab0: Using the tools',
|
||||
'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Circuit Sandbox',
|
||||
'tab_classes': []
|
||||
}]
|
||||
}, {
|
||||
'chapter_name': 'Week 1',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 4,
|
||||
'section_name': 'Administrivia and Circuit Elements',
|
||||
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Basic Circuit Analysis',
|
||||
'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Resistor Divider',
|
||||
'tab_classes': []
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Week 1 Tutorials',
|
||||
'tab_classes': []
|
||||
}]
|
||||
}, {
|
||||
'chapter_name': 'Midterm Exam',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 2,
|
||||
'section_name': 'Midterm Exam',
|
||||
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
|
||||
}]
|
||||
}]
|
||||
"""
|
||||
|
||||
course = get_course_by_id(course_id)
|
||||
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
|
||||
courseware = [{'chapter_name': c.display_name_with_default,
|
||||
'sections': [{'section_name': s.display_name_with_default,
|
||||
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
|
||||
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
|
||||
'class': t.__class__.__name__}
|
||||
for t in s.get_children()]}
|
||||
for s in c.get_children() if not s.lms.hide_from_toc]}
|
||||
for c in chapters]
|
||||
|
||||
return courseware
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
from lettuce import world
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule import seq_module, vertical_module
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
## support functions
|
||||
|
||||
|
||||
def get_courses():
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
'''
|
||||
courses = [c for c in modulestore().get_courses()
|
||||
if isinstance(c, CourseDescriptor)]
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
return courses
|
||||
|
||||
|
||||
def get_courseware_with_tabs(course_id):
|
||||
"""
|
||||
Given a course_id (string), return a courseware array of dictionaries for the
|
||||
top three levels of navigation. Same as get_courseware() except include
|
||||
the tabs on the right hand main navigation page.
|
||||
|
||||
This hides the appropriate courseware as defined by the hide_from_toc field:
|
||||
chapter.lms.hide_from_toc
|
||||
|
||||
Example:
|
||||
|
||||
[{
|
||||
'chapter_name': 'Overview',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Welcome',
|
||||
'tab_classes': []
|
||||
}, {
|
||||
'clickable_tab_count': 1,
|
||||
'section_name': 'System Usage Sequence',
|
||||
'tab_classes': ['VerticalDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Lab0: Using the tools',
|
||||
'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Circuit Sandbox',
|
||||
'tab_classes': []
|
||||
}]
|
||||
}, {
|
||||
'chapter_name': 'Week 1',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 4,
|
||||
'section_name': 'Administrivia and Circuit Elements',
|
||||
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Basic Circuit Analysis',
|
||||
'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Resistor Divider',
|
||||
'tab_classes': []
|
||||
}, {
|
||||
'clickable_tab_count': 0,
|
||||
'section_name': 'Week 1 Tutorials',
|
||||
'tab_classes': []
|
||||
}]
|
||||
}, {
|
||||
'chapter_name': 'Midterm Exam',
|
||||
'sections': [{
|
||||
'clickable_tab_count': 2,
|
||||
'section_name': 'Midterm Exam',
|
||||
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
|
||||
}]
|
||||
}]
|
||||
"""
|
||||
|
||||
course = get_course_by_id(course_id)
|
||||
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc]
|
||||
courseware = [{'chapter_name': c.display_name_with_default,
|
||||
'sections': [{'section_name': s.display_name_with_default,
|
||||
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
|
||||
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
|
||||
'class': t.__class__.__name__}
|
||||
for t in s.get_children()]}
|
||||
for s in c.get_children() if not s.lms.hide_from_toc]}
|
||||
for c in chapters]
|
||||
|
||||
return courseware
|
||||
|
||||
|
||||
def process_section(element, num_tabs=0):
|
||||
'''
|
||||
Process section reads through whatever is in 'course-content' and classifies it according to sequence module type.
|
||||
|
||||
This function is recursive
|
||||
|
||||
There are 6 types, with 6 actions.
|
||||
|
||||
Sequence Module
|
||||
-contains one child module
|
||||
|
||||
Vertical Module
|
||||
-contains other modules
|
||||
-process it and get its children, then process them
|
||||
|
||||
Capa Module
|
||||
-problem type, contains only one problem
|
||||
-for this, the most complex type, we created a separate method, process_problem
|
||||
|
||||
Video Module
|
||||
-video type, contains only one video
|
||||
-we only check to ensure that a section with class of video exists
|
||||
|
||||
HTML Module
|
||||
-html text
|
||||
-we do not check anything about it
|
||||
|
||||
Custom Tag Module
|
||||
-a custom 'hack' module type
|
||||
-there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type
|
||||
|
||||
can be used like this:
|
||||
e = world.browser.find_by_css('section.course-content section')
|
||||
process_section(e)
|
||||
|
||||
'''
|
||||
if element.has_class('xmodule_display xmodule_SequenceModule'):
|
||||
logger.debug('####### Processing xmodule_SequenceModule')
|
||||
child_modules = element.find_by_css("div>div>section[class^='xmodule']")
|
||||
for mod in child_modules:
|
||||
process_section(mod)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_VerticalModule'):
|
||||
logger.debug('####### Processing xmodule_VerticalModule')
|
||||
vert_list = element.find_by_css("li section[class^='xmodule']")
|
||||
for item in vert_list:
|
||||
process_section(item)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_CapaModule'):
|
||||
logger.debug('####### Processing xmodule_CapaModule')
|
||||
assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module"
|
||||
p = element.find_by_css("section[id^='problem']").first
|
||||
p_id = p['id']
|
||||
logger.debug('####################')
|
||||
logger.debug('id is "%s"' % p_id)
|
||||
logger.debug('####################')
|
||||
process_problem(p, p_id)
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_VideoModule'):
|
||||
logger.debug('####### Processing xmodule_VideoModule')
|
||||
assert element.find_by_css("section[class^='video']"), "No video found in Video Module"
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_HtmlModule'):
|
||||
logger.debug('####### Processing xmodule_HtmlModule')
|
||||
pass
|
||||
|
||||
elif element.has_class('xmodule_display xmodule_CustomTagModule'):
|
||||
logger.debug('####### Processing xmodule_CustomTagModule')
|
||||
pass
|
||||
|
||||
else:
|
||||
assert False, "Class for element not recognized!!"
|
||||
|
||||
|
||||
|
||||
def process_problem(element, problem_id):
|
||||
'''
|
||||
Process problem attempts to
|
||||
1) scan all the input fields and reset them
|
||||
2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect')
|
||||
3) click the 'show answer' button IF it exists and IF the answer is not already displayed
|
||||
4) enter the correct answer in each input box
|
||||
5) click the 'check' button and verify that answers are correct
|
||||
|
||||
Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM.
|
||||
The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective.
|
||||
'''
|
||||
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
|
||||
## clear out all input to ensure an incorrect result
|
||||
for field in input_fields:
|
||||
field.find_by_css("input").first.fill('')
|
||||
|
||||
## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect'
|
||||
# This would need to be reworked because multiple choice problems don't have this status
|
||||
# if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect':
|
||||
prob_xmod.find_by_css("section.action input.check").first.click()
|
||||
|
||||
## all elements become disconnected after the click
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
# Wait for the ajax reload
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
for field in input_fields:
|
||||
assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id)
|
||||
|
||||
show_button = element.find_by_css("section.action input.show").first
|
||||
## this logic is to ensure we do not accidentally hide the answers
|
||||
if show_button.value.lower() == 'show answer':
|
||||
show_button.click()
|
||||
else:
|
||||
pass
|
||||
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
|
||||
## in each field, find the answer, and send it to the field.
|
||||
## Note that this does not work if the answer type is a strange format, e.g. "either a or b"
|
||||
for field in input_fields:
|
||||
field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text)
|
||||
|
||||
prob_xmod.find_by_css("section.action input.check").first.click()
|
||||
|
||||
## assert that we entered the correct answers
|
||||
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
|
||||
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
|
||||
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
|
||||
prob_xmod = element.find_by_css("section.problem").first
|
||||
input_fields = prob_xmod.find_by_css("section[id^='input']")
|
||||
for field in input_fields:
|
||||
## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space)
|
||||
assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
|
||||
|
||||
@step('I click on View Courseware')
|
||||
def i_click_on_view_courseware(step):
|
||||
css = 'a.enter-course'
|
||||
world.browser.find_by_css(css).first.click()
|
||||
world.css_click('a.enter-course')
|
||||
|
||||
|
||||
@step('I click on the "([^"]*)" tab$')
|
||||
def i_click_on_the_tab(step, tab):
|
||||
world.browser.find_link_by_partial_text(tab).first.click()
|
||||
def i_click_on_the_tab(step, tab_text):
|
||||
world.click_link(tab_text)
|
||||
world.save_the_html()
|
||||
|
||||
|
||||
@step('I visit the courseware URL$')
|
||||
def i_visit_the_course_info_url(step):
|
||||
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
|
||||
world.browser.visit(url)
|
||||
world.visit('/courses/MITx/6.002x/2012_Fall/courseware')
|
||||
|
||||
|
||||
@step(u'I do not see "([^"]*)" anywhere on the page')
|
||||
@@ -27,18 +27,15 @@ def i_do_not_see_text_anywhere_on_the_page(step, text):
|
||||
|
||||
@step(u'I am on the dashboard page$')
|
||||
def i_am_on_the_dashboard_page(step):
|
||||
assert world.browser.is_element_present_by_css('section.courses')
|
||||
assert world.browser.url == django_url('/dashboard')
|
||||
assert world.is_css_present('section.courses')
|
||||
assert world.url_equals('/dashboard')
|
||||
|
||||
|
||||
@step('the "([^"]*)" tab is active$')
|
||||
def the_tab_is_active(step, tab):
|
||||
css = '.course-tabs a.active'
|
||||
active_tab = world.browser.find_by_css(css)
|
||||
assert (active_tab.text == tab)
|
||||
def the_tab_is_active(step, tab_text):
|
||||
assert world.css_text('.course-tabs a.active') == tab_text
|
||||
|
||||
|
||||
@step('the login dialog is visible$')
|
||||
def login_dialog_visible(step):
|
||||
css = 'form#login_form.login_form'
|
||||
assert world.browser.find_by_css(css).visible
|
||||
assert world.css_visible('form#login_form.login_form')
|
||||
|
||||
@@ -3,10 +3,10 @@ Feature: All the high level tabs should work
|
||||
As a student
|
||||
I want to navigate through the high level tabs
|
||||
|
||||
Scenario: I can navigate to all high -level tabs in a course
|
||||
Scenario: I can navigate to all high - level tabs in a course
|
||||
Given: I am registered for the course "6.002x"
|
||||
And The course "6.002x" has extra tab "Custom Tab"
|
||||
And I log in
|
||||
And I am logged in
|
||||
And I click on View Courseware
|
||||
When I click on the "<TabName>" tab
|
||||
Then the page title should contain "<PageTitle>"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import step, world
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@@ -28,9 +31,7 @@ def i_should_see_the_login_error_message(step, msg):
|
||||
|
||||
@step(u'click the dropdown arrow$')
|
||||
def click_the_dropdown(step):
|
||||
css = ".dropdown"
|
||||
e = world.browser.find_by_css(css)
|
||||
e.click()
|
||||
world.css_click('.dropdown')
|
||||
|
||||
#### helper functions
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from nose.tools import assert_equals, assert_in
|
||||
@@ -12,7 +15,7 @@ def navigate_to_an_openended_question(step):
|
||||
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
|
||||
world.browser.visit(django_url(problem))
|
||||
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
|
||||
world.browser.find_by_css(tab_css).click()
|
||||
world.css_click(tab_css)
|
||||
|
||||
|
||||
@step('I navigate to an openended question as staff$')
|
||||
@@ -22,81 +25,69 @@ def navigate_to_an_openended_question_as_staff(step):
|
||||
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
|
||||
world.browser.visit(django_url(problem))
|
||||
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
|
||||
world.browser.find_by_css(tab_css).click()
|
||||
world.css_click(tab_css)
|
||||
|
||||
|
||||
@step(u'I enter the answer "([^"]*)"$')
|
||||
def enter_the_answer_text(step, text):
|
||||
textarea_css = 'textarea'
|
||||
world.browser.find_by_css(textarea_css).first.fill(text)
|
||||
world.css_fill('textarea', text)
|
||||
|
||||
|
||||
@step(u'I submit the answer "([^"]*)"$')
|
||||
def i_submit_the_answer_text(step, text):
|
||||
textarea_css = 'textarea'
|
||||
world.browser.find_by_css(textarea_css).first.fill(text)
|
||||
check_css = 'input.check'
|
||||
world.browser.find_by_css(check_css).click()
|
||||
world.css_fill('textarea', text)
|
||||
world.css_click('input.check')
|
||||
|
||||
|
||||
@step('I click the link for full output$')
|
||||
def click_full_output_link(step):
|
||||
link_css = 'a.full'
|
||||
world.browser.find_by_css(link_css).first.click()
|
||||
world.css_click('a.full')
|
||||
|
||||
|
||||
@step(u'I visit the staff grading page$')
|
||||
def i_visit_the_staff_grading_page(step):
|
||||
# course_u = '/courses/MITx/3.091x/2012_Fall'
|
||||
# sg_url = '%s/staff_grading' % course_u
|
||||
world.browser.click_link_by_text('Instructor')
|
||||
world.browser.click_link_by_text('Staff grading')
|
||||
# world.browser.visit(django_url(sg_url))
|
||||
world.click_link('Instructor')
|
||||
world.click_link('Staff grading')
|
||||
|
||||
|
||||
@step(u'I see the grader message "([^"]*)"$')
|
||||
def see_grader_message(step, msg):
|
||||
message_css = 'div.external-grader-message'
|
||||
grader_msg = world.browser.find_by_css(message_css).text
|
||||
assert_in(msg, grader_msg)
|
||||
assert_in(msg, world.css_text(message_css))
|
||||
|
||||
|
||||
@step(u'I see the grader status "([^"]*)"$')
|
||||
def see_the_grader_status(step, status):
|
||||
status_css = 'div.grader-status'
|
||||
grader_status = world.browser.find_by_css(status_css).text
|
||||
assert_equals(status, grader_status)
|
||||
assert_equals(status, world.css_text(status_css))
|
||||
|
||||
|
||||
@step('I see the red X$')
|
||||
def see_the_red_x(step):
|
||||
x_css = 'div.grader-status > span.incorrect'
|
||||
assert world.browser.find_by_css(x_css)
|
||||
assert world.is_css_present('div.grader-status > span.incorrect')
|
||||
|
||||
|
||||
@step(u'I see the grader score "([^"]*)"$')
|
||||
def see_the_grader_score(step, score):
|
||||
score_css = 'div.result-output > p'
|
||||
score_text = world.browser.find_by_css(score_css).text
|
||||
score_text = world.css_text(score_css)
|
||||
assert_equals(score_text, 'Score: %s' % score)
|
||||
|
||||
|
||||
@step('I see the link for full output$')
|
||||
def see_full_output_link(step):
|
||||
link_css = 'a.full'
|
||||
assert world.browser.find_by_css(link_css)
|
||||
assert world.is_css_present('a.full')
|
||||
|
||||
|
||||
@step('I see the spelling grading message "([^"]*)"$')
|
||||
def see_spelling_msg(step, msg):
|
||||
spelling_css = 'div.spelling'
|
||||
spelling_msg = world.browser.find_by_css(spelling_css).text
|
||||
spelling_msg = world.css_text('div.spelling')
|
||||
assert_equals('Spelling: %s' % msg, spelling_msg)
|
||||
|
||||
|
||||
@step(u'my answer is queued for instructor grading$')
|
||||
def answer_is_queued_for_instructor_grading(step):
|
||||
list_css = 'ul.problem-list > li > a'
|
||||
actual_msg = world.browser.find_by_css(list_css).text
|
||||
actual_msg = world.css_text(list_css)
|
||||
expected_msg = "(0 graded, 1 pending)"
|
||||
assert_in(expected_msg, actual_msg)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
Feature: Answer choice problems
|
||||
Feature: Answer problems
|
||||
As a student in an edX course
|
||||
In order to test my understanding of the material
|
||||
I want to answer choice based problems
|
||||
I want to answer problems
|
||||
|
||||
Scenario: I can answer a problem correctly
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
Given External graders respond "correct"
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "correctly"
|
||||
Then My "<ProblemType>" answer is marked "correct"
|
||||
And The "<ProblemType>" problem displays a "correct" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -17,11 +19,14 @@ Feature: Answer choice problems
|
||||
| numerical |
|
||||
| formula |
|
||||
| script |
|
||||
| code |
|
||||
|
||||
Scenario: I can answer a problem incorrectly
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
Given External graders respond "incorrect"
|
||||
And I am viewing a "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "incorrectly"
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
And The "<ProblemType>" problem displays a "incorrect" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -32,11 +37,13 @@ Feature: Answer choice problems
|
||||
| numerical |
|
||||
| formula |
|
||||
| script |
|
||||
| code |
|
||||
|
||||
Scenario: I can submit a blank answer
|
||||
Given I am viewing a "<ProblemType>" problem
|
||||
When I check a problem
|
||||
Then My "<ProblemType>" answer is marked "incorrect"
|
||||
And The "<ProblemType>" problem displays a "blank" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType |
|
||||
@@ -54,6 +61,7 @@ Feature: Answer choice problems
|
||||
And I answer a "<ProblemType>" problem "<Correctness>ly"
|
||||
When I reset the problem
|
||||
Then My "<ProblemType>" answer is marked "unanswered"
|
||||
And The "<ProblemType>" problem displays a "blank" answer
|
||||
|
||||
Examples:
|
||||
| ProblemType | Correctness |
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
'''
|
||||
Steps for problem.feature lettuce tests
|
||||
'''
|
||||
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from selenium.webdriver.support.ui import Select
|
||||
import random
|
||||
import textwrap
|
||||
from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
|
||||
from terrain.factories import ItemFactory
|
||||
from common import i_am_registered_for_the_course, \
|
||||
TEST_SECTION_NAME, section_location
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
|
||||
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
|
||||
StringResponseXMLFactory, NumericalResponseXMLFactory, \
|
||||
FormulaResponseXMLFactory, CustomResponseXMLFactory
|
||||
ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
|
||||
StringResponseXMLFactory, NumericalResponseXMLFactory, \
|
||||
FormulaResponseXMLFactory, CustomResponseXMLFactory, \
|
||||
CodeResponseXMLFactory
|
||||
|
||||
# Factories from capa.tests.response_xml_factory that we will use
|
||||
# to generate the problem XML, with the keyword args used to configure
|
||||
@@ -26,7 +33,7 @@ PROBLEM_FACTORY_DICT = {
|
||||
'kwargs': {
|
||||
'question_text': 'The correct answer is Choice 3',
|
||||
'choices': [False, False, True, False],
|
||||
'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}},
|
||||
'choice_names': ['choice_0', 'choice_1', 'choice_2', 'choice_3']}},
|
||||
|
||||
'checkbox': {
|
||||
'factory': ChoiceResponseXMLFactory(),
|
||||
@@ -78,10 +85,19 @@ PROBLEM_FACTORY_DICT = {
|
||||
a2=0
|
||||
return (a1+a2)==int(expect)
|
||||
""")}},
|
||||
'code': {
|
||||
'factory': CodeResponseXMLFactory(),
|
||||
'kwargs': {
|
||||
'question_text': 'Submit code to an external grader',
|
||||
'initial_display': 'print "Hello world!"',
|
||||
'grader_payload': '{"grader": "ps1/Spring2013/test_grader.py"}', }},
|
||||
}
|
||||
|
||||
|
||||
def add_problem_to_course(course, problem_type):
|
||||
'''
|
||||
Add a problem to the course we have created using factories.
|
||||
'''
|
||||
|
||||
assert(problem_type in PROBLEM_FACTORY_DICT)
|
||||
|
||||
@@ -92,11 +108,12 @@ def add_problem_to_course(course, problem_type):
|
||||
# Create a problem item using our generated XML
|
||||
# We set rerandomize=always in the metadata so that the "Reset" button
|
||||
# will appear.
|
||||
problem_item = ItemFactory.create(parent_location=section_location(course),
|
||||
template="i4x://edx/templates/problem/Blank_Common_Problem",
|
||||
display_name=str(problem_type),
|
||||
data=problem_xml,
|
||||
metadata={'rerandomize': 'always'})
|
||||
template_name = "i4x://edx/templates/problem/Blank_Common_Problem"
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name=str(problem_type),
|
||||
data=problem_xml,
|
||||
metadata={'rerandomize': 'always'})
|
||||
|
||||
|
||||
@step(u'I am viewing a "([^"]*)" problem')
|
||||
@@ -116,6 +133,19 @@ def view_problem(step, problem_type):
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
@step(u'External graders respond "([^"]*)"')
|
||||
def set_external_grader_response(step, correctness):
|
||||
assert(correctness in ['correct', 'incorrect'])
|
||||
|
||||
response_dict = {'correct': True if correctness == 'correct' else False,
|
||||
'score': 1 if correctness == 'correct' else 0,
|
||||
'msg': 'Your problem was graded %s' % correctness}
|
||||
|
||||
# Set the fake xqueue server to always respond
|
||||
# correct/incorrect when asked to grade a problem
|
||||
world.xqueue_server.set_grade_response(response_dict)
|
||||
|
||||
|
||||
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
|
||||
def answer_problem(step, problem_type, correctness):
|
||||
""" Mark a given problem type correct or incorrect, then submit it.
|
||||
@@ -133,9 +163,9 @@ def answer_problem(step, problem_type, correctness):
|
||||
|
||||
elif problem_type == "multiple choice":
|
||||
if correctness == 'correct':
|
||||
inputfield('multiple choice', choice='choice_3').check()
|
||||
else:
|
||||
inputfield('multiple choice', choice='choice_2').check()
|
||||
else:
|
||||
inputfield('multiple choice', choice='choice_1').check()
|
||||
|
||||
elif problem_type == "checkbox":
|
||||
if correctness == 'correct':
|
||||
@@ -145,11 +175,13 @@ def answer_problem(step, problem_type, correctness):
|
||||
inputfield('checkbox', choice='choice_3').check()
|
||||
|
||||
elif problem_type == 'string':
|
||||
textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
|
||||
textvalue = 'correct string' if correctness == 'correct' \
|
||||
else 'incorrect'
|
||||
inputfield('string').fill(textvalue)
|
||||
|
||||
elif problem_type == 'numerical':
|
||||
textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
|
||||
textvalue = "pi + 1" if correctness == 'correct' \
|
||||
else str(random.randint(-2, 2))
|
||||
inputfield('numerical').fill(textvalue)
|
||||
|
||||
elif problem_type == 'formula':
|
||||
@@ -169,84 +201,154 @@ def answer_problem(step, problem_type, correctness):
|
||||
inputfield('script', input_num=1).fill(str(first_addend))
|
||||
inputfield('script', input_num=2).fill(str(second_addend))
|
||||
|
||||
elif problem_type == 'code':
|
||||
# The fake xqueue server is configured to respond
|
||||
# correct / incorrect no matter what we submit.
|
||||
# Furthermore, since the inline code response uses
|
||||
# JavaScript to make the code display nicely, it's difficult
|
||||
# to programatically input text
|
||||
# (there's not <textarea> we can just fill text into)
|
||||
# For this reason, we submit the initial code in the response
|
||||
# (configured in the problem XML above)
|
||||
pass
|
||||
|
||||
# Submit the problem
|
||||
check_problem(step)
|
||||
|
||||
|
||||
@step(u'The "([^"]*)" problem displays a "([^"]*)" answer')
|
||||
def assert_problem_has_answer(step, problem_type, answer_class):
|
||||
'''
|
||||
Assert that the problem is displaying a particular answer.
|
||||
These correspond to the same correct/incorrect
|
||||
answers we set in answer_problem()
|
||||
|
||||
We can also check that a problem has been left blank
|
||||
by setting answer_class='blank'
|
||||
'''
|
||||
assert answer_class in ['correct', 'incorrect', 'blank']
|
||||
|
||||
if problem_type == "drop down":
|
||||
if answer_class == 'blank':
|
||||
assert world.browser.is_element_not_present_by_css('option[selected="true"]')
|
||||
else:
|
||||
actual = world.browser.find_by_css('option[selected="true"]').value
|
||||
expected = 'Option 2' if answer_class == 'correct' else 'Option 3'
|
||||
assert actual == expected
|
||||
|
||||
elif problem_type == "multiple choice":
|
||||
if answer_class == 'correct':
|
||||
assert_checked('multiple choice', ['choice_2'])
|
||||
elif answer_class == 'incorrect':
|
||||
assert_checked('multiple choice', ['choice_1'])
|
||||
else:
|
||||
assert_checked('multiple choice', [])
|
||||
|
||||
elif problem_type == "checkbox":
|
||||
if answer_class == 'correct':
|
||||
assert_checked('checkbox', ['choice_0', 'choice_2'])
|
||||
elif answer_class == 'incorrect':
|
||||
assert_checked('checkbox', ['choice_3'])
|
||||
else:
|
||||
assert_checked('checkbox', [])
|
||||
|
||||
elif problem_type == 'string':
|
||||
if answer_class == 'blank':
|
||||
expected = ''
|
||||
else:
|
||||
expected = 'correct string' if answer_class == 'correct' \
|
||||
else 'incorrect'
|
||||
|
||||
assert_textfield('string', expected)
|
||||
|
||||
elif problem_type == 'formula':
|
||||
if answer_class == 'blank':
|
||||
expected = ''
|
||||
else:
|
||||
expected = "x^2+2*x+y" if answer_class == 'correct' else 'x^2'
|
||||
|
||||
assert_textfield('formula', expected)
|
||||
|
||||
else:
|
||||
# The other response types use random data,
|
||||
# which would be difficult to check
|
||||
# We trade input value coverage in the other tests for
|
||||
# input type coverage in this test.
|
||||
pass
|
||||
|
||||
|
||||
@step(u'I check a problem')
|
||||
def check_problem(step):
|
||||
world.browser.find_by_css("input.check").click()
|
||||
world.css_click("input.check")
|
||||
|
||||
|
||||
@step(u'I reset the problem')
|
||||
def reset_problem(step):
|
||||
world.browser.find_by_css('input.reset').click()
|
||||
world.css_click('input.reset')
|
||||
|
||||
|
||||
# Dictionaries that map problem types to the css selectors
|
||||
# for correct/incorrect/unanswered marks.
|
||||
# The elements are lists of selectors because a particular problem type
|
||||
# might be marked in multiple ways.
|
||||
# For example, multiple choice is marked incorrect differently
|
||||
# depending on whether the user selects an incorrect
|
||||
# item or submits without selecting any item)
|
||||
CORRECTNESS_SELECTORS = {
|
||||
'correct': {'drop down': ['span.correct'],
|
||||
'multiple choice': ['label.choicegroup_correct'],
|
||||
'checkbox': ['span.correct'],
|
||||
'string': ['div.correct'],
|
||||
'numerical': ['div.correct'],
|
||||
'formula': ['div.correct'],
|
||||
'script': ['div.correct'],
|
||||
'code': ['span.correct']},
|
||||
|
||||
'incorrect': {'drop down': ['span.incorrect'],
|
||||
'multiple choice': ['label.choicegroup_incorrect',
|
||||
'span.incorrect'],
|
||||
'checkbox': ['span.incorrect'],
|
||||
'string': ['div.incorrect'],
|
||||
'numerical': ['div.incorrect'],
|
||||
'formula': ['div.incorrect'],
|
||||
'script': ['div.incorrect'],
|
||||
'code': ['span.incorrect']},
|
||||
|
||||
'unanswered': {'drop down': ['span.unanswered'],
|
||||
'multiple choice': ['span.unanswered'],
|
||||
'checkbox': ['span.unanswered'],
|
||||
'string': ['div.unanswered'],
|
||||
'numerical': ['div.unanswered'],
|
||||
'formula': ['div.unanswered'],
|
||||
'script': ['div.unanswered'],
|
||||
'code': ['span.unanswered']}}
|
||||
|
||||
|
||||
@step(u'My "([^"]*)" answer is marked "([^"]*)"')
|
||||
def assert_answer_mark(step, problem_type, correctness):
|
||||
""" Assert that the expected answer mark is visible for a given problem type.
|
||||
"""
|
||||
Assert that the expected answer mark is visible
|
||||
for a given problem type.
|
||||
|
||||
*problem_type* is a string identifying the type of problem (e.g. 'drop down')
|
||||
*correctness* is in ['correct', 'incorrect', 'unanswered']
|
||||
"""
|
||||
|
||||
Asserting that a problem is marked 'unanswered' means that
|
||||
the problem is NOT marked correct and NOT marked incorrect.
|
||||
This can occur, for example, if the user has reset the problem. """
|
||||
# Determine which selector(s) to look for based on correctness
|
||||
assert(correctness in CORRECTNESS_SELECTORS)
|
||||
selector_dict = CORRECTNESS_SELECTORS[correctness]
|
||||
assert(problem_type in selector_dict)
|
||||
|
||||
# Dictionaries that map problem types to the css selectors
|
||||
# for correct/incorrect marks.
|
||||
# The elements are lists of selectors because a particular problem type
|
||||
# might be marked in multiple ways.
|
||||
# For example, multiple choice is marked incorrect differently
|
||||
# depending on whether the user selects an incorrect
|
||||
# item or submits without selecting any item)
|
||||
correct_selectors = {'drop down': ['span.correct'],
|
||||
'multiple choice': ['label.choicegroup_correct'],
|
||||
'checkbox': ['span.correct'],
|
||||
'string': ['div.correct'],
|
||||
'numerical': ['div.correct'],
|
||||
'formula': ['div.correct'],
|
||||
'script': ['div.correct'], }
|
||||
# At least one of the correct selectors should be present
|
||||
for sel in selector_dict[problem_type]:
|
||||
has_expected = world.is_css_present(sel)
|
||||
|
||||
incorrect_selectors = {'drop down': ['span.incorrect'],
|
||||
'multiple choice': ['label.choicegroup_incorrect',
|
||||
'span.incorrect'],
|
||||
'checkbox': ['span.incorrect'],
|
||||
'string': ['div.incorrect'],
|
||||
'numerical': ['div.incorrect'],
|
||||
'formula': ['div.incorrect'],
|
||||
'script': ['div.incorrect']}
|
||||
# As soon as we find the selector, break out of the loop
|
||||
if has_expected:
|
||||
break
|
||||
|
||||
assert(correctness in ['correct', 'incorrect', 'unanswered'])
|
||||
assert(problem_type in correct_selectors and problem_type in incorrect_selectors)
|
||||
|
||||
# Assert that the question has the expected mark
|
||||
# (either correct or incorrect)
|
||||
if correctness in ["correct", "incorrect"]:
|
||||
|
||||
selector_dict = correct_selectors if correctness == "correct" else incorrect_selectors
|
||||
|
||||
# At least one of the correct selectors should be present
|
||||
for sel in selector_dict[problem_type]:
|
||||
has_expected_mark = world.browser.is_element_present_by_css(sel, wait_time=4)
|
||||
|
||||
# As soon as we find the selector, break out of the loop
|
||||
if has_expected_mark:
|
||||
break
|
||||
|
||||
# Expect that we found the right mark (correct or incorrect)
|
||||
assert(has_expected_mark)
|
||||
|
||||
# Assert that the question has neither correct nor incorrect
|
||||
# because it is unanswered (possibly reset)
|
||||
else:
|
||||
# Get all the correct/incorrect selectors for this problem type
|
||||
selector_list = correct_selectors[problem_type] + incorrect_selectors[problem_type]
|
||||
|
||||
# Assert that none of the correct/incorrect selectors are present
|
||||
for sel in selector_list:
|
||||
assert(world.browser.is_element_not_present_by_css(sel, wait_time=4))
|
||||
# Expect that we found the expected selector
|
||||
assert(has_expected)
|
||||
|
||||
|
||||
def inputfield(problem_type, choice=None, input_num=1):
|
||||
@@ -258,14 +360,38 @@ def inputfield(problem_type, choice=None, input_num=1):
|
||||
of checkboxes. """
|
||||
|
||||
sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" %
|
||||
(problem_type.replace(" ", "_"), str(input_num)))
|
||||
(problem_type.replace(" ", "_"), str(input_num)))
|
||||
|
||||
if choice is not None:
|
||||
base = "_choice_" if problem_type == "multiple choice" else "_"
|
||||
sel = sel + base + str(choice)
|
||||
|
||||
|
||||
# If the input element doesn't exist, fail immediately
|
||||
assert(world.browser.is_element_present_by_css(sel, wait_time=4))
|
||||
assert world.is_css_present(sel)
|
||||
|
||||
# Retrieve the input element
|
||||
return world.browser.find_by_css(sel)
|
||||
|
||||
|
||||
def assert_checked(problem_type, choices):
|
||||
'''
|
||||
Assert that choice names given in *choices* are the only
|
||||
ones checked.
|
||||
|
||||
Works for both radio and checkbox problems
|
||||
'''
|
||||
|
||||
all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
for this_choice in all_choices:
|
||||
element = inputfield(problem_type, choice=this_choice)
|
||||
|
||||
if this_choice in choices:
|
||||
assert element.checked
|
||||
else:
|
||||
assert not element.checked
|
||||
|
||||
|
||||
def assert_textfield(problem_type, expected_text, input_num=1):
|
||||
element = inputfield(problem_type, input_num=input_num)
|
||||
assert element.value == expected_text
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import TEST_COURSE_ORG, TEST_COURSE_NAME
|
||||
@@ -13,17 +16,17 @@ def i_register_for_the_course(step, course):
|
||||
register_link = intro_section.find_by_css('a.register')
|
||||
register_link.click()
|
||||
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard')
|
||||
assert world.is_css_present('section.container.dashboard')
|
||||
|
||||
|
||||
@step(u'I should see the course numbered "([^"]*)" in my dashboard$')
|
||||
def i_should_see_that_course_in_my_dashboard(step, course):
|
||||
course_link_css = 'section.my-courses a[href*="%s"]' % course
|
||||
assert world.browser.is_element_present_by_css(course_link_css)
|
||||
assert world.is_css_present(course_link_css)
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" button in the Unenroll dialog')
|
||||
def i_press_the_button_in_the_unenroll_dialog(step, value):
|
||||
button_css = 'section#unenroll-modal input[value="%s"]' % value
|
||||
world.browser.find_by_css(button_css).click()
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard')
|
||||
world.css_click(button_css)
|
||||
assert world.is_css_present('section.container.dashboard')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from lettuce import world, step
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
|
||||
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
|
||||
@@ -22,4 +24,4 @@ def i_check_checkbox(step, checkbox):
|
||||
@step('I should see "([^"]*)" in the dashboard banner$')
|
||||
def i_should_see_text_in_the_dashboard_banner_section(step, text):
|
||||
css_selector = "section.dashboard-banner h2"
|
||||
assert (text in world.browser.find_by_css(css_selector).text)
|
||||
assert (text in world.css_text(css_selector))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from re import sub
|
||||
from nose.tools import assert_equals
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from courses import *
|
||||
from common import *
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
@@ -32,20 +35,20 @@ def i_verify_all_the_content_of_each_course(step):
|
||||
pass
|
||||
|
||||
for test_course in registered_courses:
|
||||
test_course.find_by_css('a').click()
|
||||
test_course.css_click('a')
|
||||
check_for_errors()
|
||||
|
||||
# Get the course. E.g. 'MITx/6.002x/2012_Fall'
|
||||
current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url))
|
||||
validate_course(current_course, ids)
|
||||
|
||||
world.browser.find_link_by_text('Courseware').click()
|
||||
assert world.browser.is_element_present_by_id('accordion', wait_time=2)
|
||||
world.click_link('Courseware')
|
||||
assert world.is_css_present('accordion')
|
||||
check_for_errors()
|
||||
browse_course(current_course)
|
||||
|
||||
# clicking the user link gets you back to the user's home page
|
||||
world.browser.find_by_css('.user-link').click()
|
||||
world.css_click('.user-link')
|
||||
check_for_errors()
|
||||
|
||||
|
||||
@@ -81,7 +84,7 @@ def browse_course(course_id):
|
||||
num_rendered_sections = len(rendered_sections)
|
||||
|
||||
msg = ('%d sections expected, %d sections found on page, %s - %d - %s' %
|
||||
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
|
||||
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
|
||||
#logger.debug(msg)
|
||||
assert num_sections == num_rendered_sections, msg
|
||||
|
||||
@@ -94,7 +97,7 @@ def browse_course(course_id):
|
||||
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
|
||||
|
||||
## sometimes the course-content takes a long time to load
|
||||
assert world.browser.is_element_present_by_css('.course-content', wait_time=5)
|
||||
assert world.is_css_present('.course-content')
|
||||
|
||||
## look for server error div
|
||||
check_for_errors()
|
||||
@@ -112,7 +115,7 @@ def browse_course(course_id):
|
||||
num_rendered_tabs = 0
|
||||
|
||||
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
|
||||
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
|
||||
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
|
||||
#logger.debug(msg)
|
||||
|
||||
# Save the HTML to a file for later comparison
|
||||
@@ -137,7 +140,7 @@ def browse_course(course_id):
|
||||
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
|
||||
num_rendered_items = len(rendered_items)
|
||||
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
|
||||
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
|
||||
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
|
||||
#logger.debug(msg)
|
||||
assert tab_children == num_rendered_items, msg
|
||||
|
||||
|
||||
35
lms/djangoapps/courseware/features/xqueue_setup.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
@before.all
|
||||
def setup_mock_xqueue_server():
|
||||
|
||||
# Retrieve the local port from settings
|
||||
server_port = settings.XQUEUE_PORT
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockXQueueServer(server_port)
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.xqueue_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_xqueue_server(total):
|
||||
|
||||
# Stop the xqueue server and free up the port
|
||||
world.xqueue_server.shutdown()
|
||||
@@ -10,10 +10,9 @@ from django.contrib.auth.models import User
|
||||
|
||||
from .model_data import ModelDataCache, LmsKeyValueStore
|
||||
from xblock.core import Scope
|
||||
from .module_render import get_module
|
||||
from .module_render import get_module, get_module_for_descriptor
|
||||
from xmodule import graders
|
||||
from xmodule.capa_module import CapaModule
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.graders import Score
|
||||
from .models import StudentModule
|
||||
|
||||
@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
|
||||
else:
|
||||
return descriptor.get_children()
|
||||
|
||||
|
||||
stack = [descriptor]
|
||||
|
||||
while len(stack) > 0:
|
||||
@@ -66,7 +64,7 @@ def yield_problems(request, course, student):
|
||||
).values_list('module_state_key', flat=True))
|
||||
|
||||
sections_to_list = []
|
||||
for section_format, sections in grading_context['graded_sections'].iteritems():
|
||||
for _, sections in grading_context['graded_sections'].iteritems():
|
||||
for section in sections:
|
||||
|
||||
section_descriptor = section['section_descriptor']
|
||||
@@ -123,7 +121,7 @@ def answer_distributions(request, course):
|
||||
|
||||
def grade(student, request, course, model_data_cache=None, keep_raw_scores=False):
|
||||
"""
|
||||
This grades a student as quickly as possible. It retuns the
|
||||
This grades a student as quickly as possible. It returns the
|
||||
output from the course grader, augmented with the final letter
|
||||
grade. The keys in the output are:
|
||||
|
||||
@@ -158,10 +156,16 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
|
||||
should_grade_section = False
|
||||
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
|
||||
for moduledescriptor in section['xmoduledescriptors']:
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.)
|
||||
if moduledescriptor.always_recalculate_grades:
|
||||
should_grade_section = True
|
||||
break
|
||||
|
||||
# Create a fake key to pull out a StudentModule object from the ModelDataCache
|
||||
|
||||
key = LmsKeyValueStore.Key(
|
||||
Scope.student_state,
|
||||
Scope.user_state,
|
||||
student.id,
|
||||
moduledescriptor.location,
|
||||
None
|
||||
@@ -174,10 +178,10 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
|
||||
scores = []
|
||||
|
||||
def create_module(descriptor):
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
'''creates an XModule instance given a descriptor'''
|
||||
# TODO: We need the request to pass into here. If we could forego that, our arguments
|
||||
# would be simpler
|
||||
return get_module(student, request, descriptor.location,
|
||||
model_data_cache, course.id)
|
||||
return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id)
|
||||
|
||||
for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module):
|
||||
|
||||
@@ -198,18 +202,18 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
|
||||
|
||||
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
|
||||
|
||||
section_total, graded_total = graders.aggregate_scores(scores, section_name)
|
||||
_, graded_total = graders.aggregate_scores(scores, section_name)
|
||||
if keep_raw_scores:
|
||||
raw_scores += scores
|
||||
else:
|
||||
section_total = Score(0.0, 1.0, False, section_name)
|
||||
graded_total = Score(0.0, 1.0, True, section_name)
|
||||
|
||||
#Add the graded total to totaled_scores
|
||||
if graded_total.possible > 0:
|
||||
format_scores.append(graded_total)
|
||||
else:
|
||||
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location))
|
||||
log.exception("Unable to grade a section with a total possible score of zero. " +
|
||||
str(section_descriptor.location))
|
||||
|
||||
totaled_scores[section_format] = format_scores
|
||||
|
||||
@@ -275,12 +279,9 @@ def progress_summary(student, request, course, model_data_cache):
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
# TODO: We need the request to pass into here. If we could forego that, our arguments
|
||||
# would be simpler
|
||||
course_module = get_module(student, request,
|
||||
course.location, model_data_cache,
|
||||
course.id, depth=None)
|
||||
course_module = get_module(student, request, course.location, model_data_cache, course.id, depth=None)
|
||||
if not course_module:
|
||||
# This student must not have access to the course.
|
||||
return None
|
||||
@@ -311,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache):
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
scores.append(Score(correct, total, graded,
|
||||
module_descriptor.display_name_with_default))
|
||||
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
|
||||
|
||||
scores.reverse()
|
||||
section_total, graded_total = graders.aggregate_scores(
|
||||
section_total, _ = graders.aggregate_scores(
|
||||
scores, section_module.display_name_with_default)
|
||||
|
||||
format = section_module.lms.format if section_module.lms.format is not None else ''
|
||||
module_format = section_module.lms.format if section_module.lms.format is not None else ''
|
||||
sections.append({
|
||||
'display_name': section_module.display_name_with_default,
|
||||
'url_name': section_module.url_name,
|
||||
'scores': scores,
|
||||
'section_total': section_total,
|
||||
'format': format,
|
||||
'format': module_format,
|
||||
'due': section_module.lms.due,
|
||||
'graded': graded,
|
||||
})
|
||||
@@ -354,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
|
||||
if not user.is_authenticated():
|
||||
return (None, None)
|
||||
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.)
|
||||
if problem_descriptor.always_recalculate_grades:
|
||||
problem = module_creator(problem_descriptor)
|
||||
d = problem.get_score()
|
||||
if d is not None:
|
||||
return (d['score'], d['total'])
|
||||
score = problem.get_score()
|
||||
if score is not None:
|
||||
return (score['score'], score['total'])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
@@ -368,7 +370,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
|
||||
|
||||
# Create a fake KeyValueStore key to pull out the StudentModule
|
||||
key = LmsKeyValueStore.Key(
|
||||
Scope.student_state,
|
||||
Scope.user_state,
|
||||
user.id,
|
||||
problem_descriptor.location,
|
||||
None
|
||||
@@ -395,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
|
||||
if total is None:
|
||||
return (None, None)
|
||||
|
||||
#Now we re-weight the problem, if specified
|
||||
# Now we re-weight the problem, if specified
|
||||
weight = problem_descriptor.weight
|
||||
if weight is not None:
|
||||
if total == 0:
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
'''
|
||||
This is a one-off command aimed at fixing a temporary problem encountered where input_state was added to
|
||||
the same dict object in capa problems, so was accumulating. The fix is simply to remove input_state entry
|
||||
from state for all problems in the affected date range.
|
||||
'''
|
||||
|
||||
import json
|
||||
import logging
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from courseware.models import StudentModule, StudentModuleHistory
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
'''
|
||||
The fix here is to remove the "input_state" entry in the StudentModule objects of any problems that
|
||||
contain them. No problem is yet making use of this, and the code should do the right thing if it's
|
||||
missing (by recreating an empty dict for its value).
|
||||
|
||||
To narrow down the set of problems that might need fixing, the StudentModule
|
||||
objects to be checked is filtered down to those:
|
||||
|
||||
created < '2013-03-29 16:30:00' (the problem must have been answered before the buggy code was reverted,
|
||||
on Prod and Edge)
|
||||
modified > '2013-03-28 22:00:00' (the problem must have been visited after the bug was introduced
|
||||
on Prod and Edge)
|
||||
state like '%input_state%' (the problem must have "input_state" set).
|
||||
|
||||
This filtering is done on the production database replica, so that the larger select queries don't lock
|
||||
the real production database. The list of id values for Student Modules is written to a file, and the
|
||||
file is passed into this command. The sql file passed to mysql contains:
|
||||
|
||||
select sm.id from courseware_studentmodule sm
|
||||
where sm.modified > "2013-03-28 22:00:00"
|
||||
and sm.created < "2013-03-29 16:30:00"
|
||||
and sm.state like "%input_state%"
|
||||
and sm.module_type = 'problem';
|
||||
|
||||
'''
|
||||
|
||||
num_visited = 0
|
||||
num_changed = 0
|
||||
num_hist_visited = 0
|
||||
num_hist_changed = 0
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--save',
|
||||
action='store_true',
|
||||
dest='save_changes',
|
||||
default=False,
|
||||
help='Persist the changes that were encountered. If not set, no changes are saved.'),
|
||||
)
|
||||
|
||||
def fix_studentmodules_in_list(self, save_changes, idlist_path):
|
||||
'''Read in the list of StudentModule objects that might need fixing, and then fix each one'''
|
||||
|
||||
# open file and read id values from it:
|
||||
for line in open(idlist_path, 'r'):
|
||||
student_module_id = line.strip()
|
||||
# skip the header, if present:
|
||||
if student_module_id == 'id':
|
||||
continue
|
||||
try:
|
||||
module = StudentModule.objects.get(id=student_module_id)
|
||||
except StudentModule.DoesNotExist:
|
||||
LOG.error("Unable to find student module with id = {0}: skipping... ".format(student_module_id))
|
||||
continue
|
||||
self.remove_studentmodule_input_state(module, save_changes)
|
||||
|
||||
hist_modules = StudentModuleHistory.objects.filter(student_module_id=student_module_id)
|
||||
for hist_module in hist_modules:
|
||||
self.remove_studentmodulehistory_input_state(hist_module, save_changes)
|
||||
|
||||
if self.num_visited % 1000 == 0:
|
||||
LOG.info(" Progress: updated {0} of {1} student modules".format(self.num_changed, self.num_visited))
|
||||
LOG.info(" Progress: updated {0} of {1} student history modules".format(self.num_hist_changed,
|
||||
self.num_hist_visited))
|
||||
|
||||
@transaction.autocommit
|
||||
def remove_studentmodule_input_state(self, module, save_changes):
|
||||
''' Fix the grade assigned to a StudentModule'''
|
||||
module_state = module.state
|
||||
if module_state is None:
|
||||
# not likely, since we filter on it. But in general...
|
||||
LOG.info("No state found for {type} module {id} for student {student} in course {course_id}"
|
||||
.format(type=module.module_type, id=module.module_state_key,
|
||||
student=module.student.username, course_id=module.course_id))
|
||||
return
|
||||
|
||||
state_dict = json.loads(module_state)
|
||||
self.num_visited += 1
|
||||
|
||||
if 'input_state' not in state_dict:
|
||||
pass
|
||||
elif save_changes:
|
||||
# make the change and persist
|
||||
del state_dict['input_state']
|
||||
module.state = json.dumps(state_dict)
|
||||
module.save()
|
||||
self.num_changed += 1
|
||||
else:
|
||||
# don't make the change, but increment the count indicating the change would be made
|
||||
self.num_changed += 1
|
||||
|
||||
@transaction.autocommit
|
||||
def remove_studentmodulehistory_input_state(self, module, save_changes):
|
||||
''' Fix the grade assigned to a StudentModule'''
|
||||
module_state = module.state
|
||||
if module_state is None:
|
||||
# not likely, since we filter on it. But in general...
|
||||
LOG.info("No state found for {type} module {id} for student {student} in course {course_id}"
|
||||
.format(type=module.module_type, id=module.module_state_key,
|
||||
student=module.student.username, course_id=module.course_id))
|
||||
return
|
||||
|
||||
state_dict = json.loads(module_state)
|
||||
self.num_hist_visited += 1
|
||||
|
||||
if 'input_state' not in state_dict:
|
||||
pass
|
||||
elif save_changes:
|
||||
# make the change and persist
|
||||
del state_dict['input_state']
|
||||
module.state = json.dumps(state_dict)
|
||||
module.save()
|
||||
self.num_hist_changed += 1
|
||||
else:
|
||||
# don't make the change, but increment the count indicating the change would be made
|
||||
self.num_hist_changed += 1
|
||||
|
||||
def handle(self, *args, **options):
|
||||
'''Handle management command request'''
|
||||
if len(args) != 1:
|
||||
raise CommandError("missing idlist file")
|
||||
idlist_path = args[0]
|
||||
save_changes = options['save_changes']
|
||||
LOG.info("Starting run: reading from idlist file {0}; save_changes = {1}".format(idlist_path, save_changes))
|
||||
|
||||
self.fix_studentmodules_in_list(save_changes, idlist_path)
|
||||
|
||||
LOG.info("Finished run: updating {0} of {1} student modules".format(self.num_changed, self.num_visited))
|
||||
LOG.info("Finished run: updating {0} of {1} student history modules".format(self.num_hist_changed,
|
||||
self.num_hist_visited))
|
||||
@@ -0,0 +1,211 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for XQueue POST requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_HEAD(self):
|
||||
self._send_head()
|
||||
|
||||
def do_POST(self):
|
||||
'''
|
||||
Handle a POST request from the client
|
||||
|
||||
Sends back an immediate success/failure response.
|
||||
It then POSTS back to the client
|
||||
with grading results, as configured in MockXQueueServer.
|
||||
'''
|
||||
self._send_head()
|
||||
|
||||
# Retrieve the POST data
|
||||
post_dict = self._post_dict()
|
||||
|
||||
# Log the request
|
||||
logger.debug("XQueue received POST request %s to path %s" %
|
||||
(str(post_dict), self.path))
|
||||
|
||||
# Respond only to grading requests
|
||||
if self._is_grade_request():
|
||||
try:
|
||||
xqueue_header = json.loads(post_dict['xqueue_header'])
|
||||
xqueue_body = json.loads(post_dict['xqueue_body'])
|
||||
|
||||
callback_url = xqueue_header['lms_callback_url']
|
||||
|
||||
except KeyError:
|
||||
# If the message doesn't have a header or body,
|
||||
# then it's malformed.
|
||||
# Respond with failure
|
||||
error_msg = "XQueue received invalid grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
except ValueError:
|
||||
# If we could not decode the body or header,
|
||||
# respond with failure
|
||||
|
||||
error_msg = "XQueue could not decode grade request"
|
||||
self._send_immediate_response(False, message=error_msg)
|
||||
|
||||
else:
|
||||
# Send an immediate response of success
|
||||
# The grade request is formed correctly
|
||||
self._send_immediate_response(True)
|
||||
|
||||
# Wait a bit before POSTing back to the callback url with the
|
||||
# grade result configured by the server
|
||||
# Otherwise, the problem will not realize it's
|
||||
# queued and it will keep waiting for a response
|
||||
# indefinitely
|
||||
delayed_grade_func = lambda: self._send_grade_response(callback_url,
|
||||
xqueue_header)
|
||||
|
||||
timer = threading.Timer(2, delayed_grade_func)
|
||||
timer.start()
|
||||
|
||||
# If we get a request that's not to the grading submission
|
||||
# URL, return an error
|
||||
else:
|
||||
error_message = "Invalid request URL"
|
||||
self._send_immediate_response(False, message=error_message)
|
||||
|
||||
|
||||
def _send_head(self):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
if self._is_grade_request():
|
||||
self.send_response(200)
|
||||
else:
|
||||
self.send_response(500)
|
||||
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
|
||||
def _post_dict(self):
|
||||
'''
|
||||
Retrieve the POST parameters from the client as a dictionary
|
||||
'''
|
||||
|
||||
try:
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
|
||||
post_dict = urlparse.parse_qs(self.rfile.read(length))
|
||||
|
||||
# The POST dict will contain a list of values
|
||||
# for each key.
|
||||
# None of our parameters are lists, however,
|
||||
# so we map [val] --> val
|
||||
# If the list contains multiple entries,
|
||||
# we pick the first one
|
||||
post_dict = dict(map(lambda (key, list_val): (key, list_val[0]),
|
||||
post_dict.items()))
|
||||
|
||||
except:
|
||||
# We return an empty dict here, on the assumption
|
||||
# that when we later check that the request has
|
||||
# the correct fields, it won't find them,
|
||||
# and will therefore send an error response
|
||||
return {}
|
||||
|
||||
return post_dict
|
||||
|
||||
def _send_immediate_response(self, success, message=""):
|
||||
'''
|
||||
Send an immediate success/failure message
|
||||
back to the client
|
||||
'''
|
||||
|
||||
# Send the response indicating success/failure
|
||||
response_str = json.dumps({'return_code': 0 if success else 1,
|
||||
'content': message})
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent response %s" % response_str)
|
||||
|
||||
self.wfile.write(response_str)
|
||||
|
||||
def _send_grade_response(self, postback_url, xqueue_header):
|
||||
'''
|
||||
POST the grade response back to the client
|
||||
using the response provided by the server configuration
|
||||
'''
|
||||
response_dict = {'xqueue_header': json.dumps(xqueue_header),
|
||||
'xqueue_body': json.dumps(self.server.grade_response())}
|
||||
|
||||
# Log the response
|
||||
logger.debug("XQueue: sent grading response %s" % str(response_dict))
|
||||
|
||||
MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
|
||||
|
||||
def _is_grade_request(self):
|
||||
return 'xqueue/submit' in self.path
|
||||
|
||||
@staticmethod
|
||||
def post_to_url(url, param_dict):
|
||||
'''
|
||||
POST *param_dict* to *url*
|
||||
We make this a separate function so we can easily patch
|
||||
it during testing.
|
||||
'''
|
||||
urllib.urlopen(url, urllib.urlencode(param_dict))
|
||||
|
||||
|
||||
class MockXQueueServer(HTTPServer):
|
||||
'''
|
||||
A mock XQueue grading server that responds
|
||||
to POST requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, port_num,
|
||||
grade_response_dict={'correct': True, 'score': 1, 'msg': ''}):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*port_num* is the localhost port to listen to
|
||||
|
||||
*grade_response_dict* is a dictionary that will be JSON-serialized
|
||||
and sent in response to XQueue grading requests.
|
||||
'''
|
||||
|
||||
self.set_grade_response(grade_response_dict)
|
||||
|
||||
handler = MockXQueueRequestHandler
|
||||
address = ('', port_num)
|
||||
HTTPServer.__init__(self, address, handler)
|
||||
|
||||
def shutdown(self):
|
||||
'''
|
||||
Stop the server and free up the port
|
||||
'''
|
||||
# First call superclass shutdown()
|
||||
HTTPServer.shutdown(self)
|
||||
|
||||
# We also need to manually close the socket
|
||||
self.socket.close()
|
||||
|
||||
def grade_response(self):
|
||||
return self._grade_response
|
||||
|
||||
def set_grade_response(self, grade_response_dict):
|
||||
|
||||
# Check that the grade response has the right keys
|
||||
assert('correct' in grade_response_dict and
|
||||
'score' in grade_response_dict and
|
||||
'msg' in grade_response_dict)
|
||||
|
||||
# Wrap the message in <div> tags to ensure that it is valid XML
|
||||
grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']
|
||||
|
||||
# Save the response dictionary
|
||||
self._grade_response = grade_response_dict
|
||||
@@ -0,0 +1,84 @@
|
||||
import mock
|
||||
import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockXQueueServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the XQueue server that listens on a local
|
||||
port and responds with pre-defined grade messages.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/problems.feature
|
||||
and lms/courseware/features/problems.py
|
||||
|
||||
This is temporary and will be removed when XQueue is
|
||||
rewritten using celery.
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# This is a test of the test setup,
|
||||
# so it does not need to run as part of the unit test suite
|
||||
# You can re-enable it by commenting out the line below
|
||||
raise SkipTest
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
self.server_url = 'http://127.0.0.1:%d' % server_port
|
||||
self.server = MockXQueueServer(server_port,
|
||||
{'correct': True, 'score': 1, 'msg': ''})
|
||||
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
# Stop the server, freeing up the port
|
||||
self.server.shutdown()
|
||||
|
||||
def test_grade_request(self):
|
||||
|
||||
# Patch post_to_url() so we can intercept
|
||||
# outgoing POST requests from the server
|
||||
MockXQueueRequestHandler.post_to_url = mock.Mock()
|
||||
|
||||
# Send a grade request
|
||||
callback_url = 'http://127.0.0.1:8000/test_callback'
|
||||
|
||||
grade_header = json.dumps({'lms_callback_url': callback_url,
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'})
|
||||
|
||||
grade_body = json.dumps({'student_info': 'test',
|
||||
'grader_payload': 'test',
|
||||
'student_response': 'test'})
|
||||
|
||||
grade_request = {'xqueue_header': grade_header,
|
||||
'xqueue_body': grade_body}
|
||||
|
||||
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
|
||||
urllib.urlencode(grade_request))
|
||||
|
||||
response_dict = json.loads(response_handle.read())
|
||||
|
||||
# Expect that the response is success
|
||||
self.assertEqual(response_dict['return_code'], 0)
|
||||
|
||||
# Wait a bit before checking that the server posted back
|
||||
time.sleep(3)
|
||||
|
||||
# Expect that the server tries to post back the grading info
|
||||
xqueue_body = json.dumps({'correct': True, 'score': 1,
|
||||
'msg': '<div></div>'})
|
||||
expected_callback_dict = {'xqueue_header': grade_header,
|
||||
'xqueue_body': xqueue_body}
|
||||
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
|
||||
expected_callback_dict)
|
||||
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
Classes to provide the LMS runtime data storage to XBlocks
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections import namedtuple, defaultdict
|
||||
from itertools import chain
|
||||
@@ -14,10 +18,16 @@ from xblock.core import Scope
|
||||
|
||||
|
||||
class InvalidWriteError(Exception):
|
||||
pass
|
||||
"""
|
||||
Raised to indicate that writing to a particular key
|
||||
in the KeyValueStore is disabled
|
||||
"""
|
||||
|
||||
|
||||
def chunks(items, chunk_size):
|
||||
"""
|
||||
Yields the values from items in chunks of size chunk_size
|
||||
"""
|
||||
items = list(items)
|
||||
return (items[i:i + chunk_size] for i in xrange(0, len(items), chunk_size))
|
||||
|
||||
@@ -67,6 +77,15 @@ class ModelDataCache(object):
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
"""
|
||||
Return a list of all child descriptors down to the specified depth
|
||||
that match the descriptor filter. Includes `descriptor`
|
||||
|
||||
descriptor: The parent to search inside
|
||||
depth: The number of levels to descend, or None for infinite depth
|
||||
descriptor_filter(descriptor): A function that returns True
|
||||
if descriptor should be included in the results
|
||||
"""
|
||||
if descriptor_filter(descriptor):
|
||||
descriptors = [descriptor]
|
||||
else:
|
||||
@@ -115,13 +134,13 @@ class ModelDataCache(object):
|
||||
"""
|
||||
if scope in (Scope.children, Scope.parent):
|
||||
return []
|
||||
elif scope == Scope.student_state:
|
||||
elif scope == Scope.user_state:
|
||||
return self._chunked_query(
|
||||
StudentModule,
|
||||
'module_state_key__in',
|
||||
(descriptor.location.url() for descriptor in self.descriptors),
|
||||
course_id=self.course_id,
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
)
|
||||
elif scope == Scope.content:
|
||||
return self._chunked_query(
|
||||
@@ -140,18 +159,18 @@ class ModelDataCache(object):
|
||||
),
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
elif scope == Scope.student_preferences:
|
||||
elif scope == Scope.preferences:
|
||||
return self._chunked_query(
|
||||
XModuleStudentPrefsField,
|
||||
'module_type__in',
|
||||
set(descriptor.location.category for descriptor in self.descriptors),
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
elif scope == Scope.student_info:
|
||||
elif scope == Scope.user_info:
|
||||
return self._query(
|
||||
XModuleStudentInfoField,
|
||||
student=self.user,
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
else:
|
||||
@@ -168,27 +187,34 @@ class ModelDataCache(object):
|
||||
return scope_map
|
||||
|
||||
def _cache_key_from_kvs_key(self, key):
|
||||
if key.scope == Scope.student_state:
|
||||
"""
|
||||
Return the key used in the ModelDataCache for the specified KeyValueStore key
|
||||
"""
|
||||
if key.scope == Scope.user_state:
|
||||
return (key.scope, key.block_scope_id.url())
|
||||
elif key.scope == Scope.content:
|
||||
return (key.scope, key.block_scope_id.url(), key.field_name)
|
||||
elif key.scope == Scope.settings:
|
||||
return (key.scope, '%s-%s' % (self.course_id, key.block_scope_id.url()), key.field_name)
|
||||
elif key.scope == Scope.student_preferences:
|
||||
elif key.scope == Scope.preferences:
|
||||
return (key.scope, key.block_scope_id, key.field_name)
|
||||
elif key.scope == Scope.student_info:
|
||||
elif key.scope == Scope.user_info:
|
||||
return (key.scope, key.field_name)
|
||||
|
||||
def _cache_key_from_field_object(self, scope, field_object):
|
||||
if scope == Scope.student_state:
|
||||
"""
|
||||
Return the key used in the ModelDataCache for the specified scope and
|
||||
field
|
||||
"""
|
||||
if scope == Scope.user_state:
|
||||
return (scope, field_object.module_state_key)
|
||||
elif scope == Scope.content:
|
||||
return (scope, field_object.definition_id, field_object.field_name)
|
||||
elif scope == Scope.settings:
|
||||
return (scope, field_object.usage_id, field_object.field_name)
|
||||
elif scope == Scope.student_preferences:
|
||||
elif scope == Scope.preferences:
|
||||
return (scope, field_object.module_type, field_object.field_name)
|
||||
elif scope == Scope.student_info:
|
||||
elif scope == Scope.user_info:
|
||||
return (scope, field_object.field_name)
|
||||
|
||||
def find(self, key):
|
||||
@@ -211,13 +237,14 @@ class ModelDataCache(object):
|
||||
if field_object is not None:
|
||||
return field_object
|
||||
|
||||
if key.scope == Scope.student_state:
|
||||
if key.scope == Scope.user_state:
|
||||
field_object, _ = StudentModule.objects.get_or_create(
|
||||
course_id=self.course_id,
|
||||
student=self.user,
|
||||
module_type=key.block_scope_id.category,
|
||||
module_state_key=key.block_scope_id.url(),
|
||||
defaults={'state': json.dumps({})},
|
||||
defaults={'state': json.dumps({}),
|
||||
'module_type': key.block_scope_id.category,
|
||||
},
|
||||
)
|
||||
elif key.scope == Scope.content:
|
||||
field_object, _ = XModuleContentField.objects.get_or_create(
|
||||
@@ -229,13 +256,13 @@ class ModelDataCache(object):
|
||||
field_name=key.field_name,
|
||||
usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()),
|
||||
)
|
||||
elif key.scope == Scope.student_preferences:
|
||||
field_object, _= XModuleStudentPrefsField.objects.get_or_create(
|
||||
elif key.scope == Scope.preferences:
|
||||
field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
|
||||
field_name=key.field_name,
|
||||
module_type=key.block_scope_id,
|
||||
student=self.user,
|
||||
)
|
||||
elif key.scope == Scope.student_info:
|
||||
elif key.scope == Scope.user_info:
|
||||
field_object, _ = XModuleStudentInfoField.objects.get_or_create(
|
||||
field_name=key.field_name,
|
||||
student=self.user,
|
||||
@@ -255,12 +282,12 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
If the scope to write to is not one of the 5 named scopes:
|
||||
Scope.content
|
||||
Scope.settings
|
||||
Scope.student_state
|
||||
Scope.student_preferences
|
||||
Scope.student_info
|
||||
Scope.user_state
|
||||
Scope.preferences
|
||||
Scope.user_info
|
||||
then an InvalidScopeError will be raised.
|
||||
|
||||
Data for Scope.student_state is stored as StudentModule objects via the django orm.
|
||||
Data for Scope.user_state is stored as StudentModule objects via the django orm.
|
||||
|
||||
Data for the other scopes is stored in individual objects that are named for the
|
||||
scope involved and have the field name as a key
|
||||
@@ -271,11 +298,12 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
_allowed_scopes = (
|
||||
Scope.content,
|
||||
Scope.settings,
|
||||
Scope.student_state,
|
||||
Scope.student_preferences,
|
||||
Scope.student_info,
|
||||
Scope.user_state,
|
||||
Scope.preferences,
|
||||
Scope.user_info,
|
||||
Scope.children,
|
||||
)
|
||||
|
||||
def __init__(self, descriptor_model_data, model_data_cache):
|
||||
self._descriptor_model_data = descriptor_model_data
|
||||
self._model_data_cache = model_data_cache
|
||||
@@ -294,7 +322,7 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
if field_object is None:
|
||||
raise KeyError(key.field_name)
|
||||
|
||||
if key.scope == Scope.student_state:
|
||||
if key.scope == Scope.user_state:
|
||||
return json.loads(field_object.state)[key.field_name]
|
||||
else:
|
||||
return json.loads(field_object.value)
|
||||
@@ -308,7 +336,7 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
if key.scope not in self._allowed_scopes:
|
||||
raise InvalidScopeError(key.scope)
|
||||
|
||||
if key.scope == Scope.student_state:
|
||||
if key.scope == Scope.user_state:
|
||||
state = json.loads(field_object.state)
|
||||
state[key.field_name] = value
|
||||
field_object.state = json.dumps(state)
|
||||
@@ -328,7 +356,7 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
if field_object is None:
|
||||
raise KeyError(key.field_name)
|
||||
|
||||
if key.scope == Scope.student_state:
|
||||
if key.scope == Scope.user_state:
|
||||
state = json.loads(field_object.state)
|
||||
del state[key.field_name]
|
||||
field_object.state = json.dumps(state)
|
||||
@@ -350,11 +378,10 @@ class LmsKeyValueStore(KeyValueStore):
|
||||
if field_object is None:
|
||||
return False
|
||||
|
||||
if key.scope == Scope.student_state:
|
||||
if key.scope == Scope.user_state:
|
||||
return key.field_name in json.loads(field_object.state)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
LmsUsage = namedtuple('LmsUsage', 'id, def_id')
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ class XModuleSettingsField(models.Model):
|
||||
|
||||
class XModuleStudentPrefsField(models.Model):
|
||||
"""
|
||||
Stores data set in the Scope.student_preferences scope by an xmodule field
|
||||
Stores data set in the Scope.preferences scope by an xmodule field
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@@ -199,7 +199,7 @@ class XModuleStudentPrefsField(models.Model):
|
||||
|
||||
class XModuleStudentInfoField(models.Model):
|
||||
"""
|
||||
Stores data set in the Scope.student_preferences scope by an xmodule field
|
||||
Stores data set in the Scope.preferences scope by an xmodule field
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -8,9 +8,10 @@ from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
@@ -22,7 +23,7 @@ from .models import StudentModule
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from student.models import unique_id_for_user
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import ModuleSystem
|
||||
@@ -176,17 +177,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
# Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash.
|
||||
ajax_url = ajax_url.rstrip('/')
|
||||
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = '{proto}://{host}'.format(
|
||||
host=request.get_host(),
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
|
||||
)
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch='score_update'),
|
||||
)
|
||||
def make_xqueue_callback(dispatch='score_update'):
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = '{proto}://{host}'.format(
|
||||
host=request.get_host(),
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
|
||||
)
|
||||
xqueue_callback_url = settings.XQUEUE_INTERFACE.get('callback_url',xqueue_callback_url) # allow override
|
||||
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch=dispatch),
|
||||
)
|
||||
return xqueue_callback_url
|
||||
|
||||
# Default queuename is course-specific and is derived from the course that
|
||||
# contains the current module.
|
||||
@@ -194,14 +199,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||
|
||||
xqueue = {'interface': xqueue_interface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'construct_callback': make_xqueue_callback,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ', '_'),
|
||||
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
}
|
||||
|
||||
def get_or_default(key, default):
|
||||
getattr(settings, key, default)
|
||||
|
||||
#This is a hacky way to pass settings to the combined open ended xmodule
|
||||
#It needs an S3 interface to upload images to S3
|
||||
#It needs the open ended grading interface in order to get peer grading to be done
|
||||
@@ -217,12 +219,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING
|
||||
if is_descriptor_combined_open_ended:
|
||||
s3_interface = {
|
||||
'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''),
|
||||
'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''),
|
||||
'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','')
|
||||
'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''),
|
||||
'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''),
|
||||
'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended')
|
||||
}
|
||||
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
@@ -403,6 +404,9 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
if not Location.is_valid(location):
|
||||
raise Http404("Invalid location")
|
||||
|
||||
if not request.user.is_authenticated():
|
||||
raise PermissionDenied
|
||||
|
||||
# Check for submitted files and basic file size checks
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
@@ -434,9 +438,19 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, p)
|
||||
|
||||
# If we can't find the module, respond with a 404
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
|
||||
# For XModule-specific errors, we respond with 400
|
||||
except ProcessingError:
|
||||
log.warning("Module encountered an error while prcessing AJAX call",
|
||||
exc_info=True)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# If any other error occurred, re-raise it to trigger a 500 response
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
107
lms/djangoapps/courseware/tests/test_login.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import Registration, UserProfile
|
||||
import json
|
||||
|
||||
class LoginTest(TestCase):
|
||||
'''
|
||||
Test student.views.login_user() view
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create one user and save it to the database
|
||||
self.user = User.objects.create_user('test', 'test@edx.org', 'test_password')
|
||||
self.user.is_active = True
|
||||
self.user.save()
|
||||
|
||||
# Create a registration for the user
|
||||
Registration().register(self.user)
|
||||
|
||||
# Create a profile for the user
|
||||
UserProfile(user=self.user).save()
|
||||
|
||||
# Create the test client
|
||||
self.client = Client()
|
||||
|
||||
# Store the login url
|
||||
self.url = reverse('login')
|
||||
|
||||
def test_login_success(self):
|
||||
response = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
def test_login_success_unicode_email(self):
|
||||
unicode_email = u'test@edx.org' + unichr(40960)
|
||||
|
||||
self.user.email = unicode_email
|
||||
self.user.save()
|
||||
|
||||
response = self._login_response(unicode_email, 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
|
||||
def test_login_fail_no_user_exists(self):
|
||||
response = self._login_response('not_a_user@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_fail_wrong_password(self):
|
||||
response = self._login_response('test@edx.org', 'wrong_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_not_activated(self):
|
||||
|
||||
# De-activate the user
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
|
||||
# Should now be unable to login
|
||||
response = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value="This account has not been activated")
|
||||
|
||||
|
||||
def test_login_unicode_email(self):
|
||||
unicode_email = u'test@edx.org' + unichr(40960)
|
||||
response = self._login_response(unicode_email, 'test_password')
|
||||
self._assert_response(response, success=False)
|
||||
|
||||
def test_login_unicode_password(self):
|
||||
unicode_password = u'test_password' + unichr(1972)
|
||||
response = self._login_response('test@edx.org', unicode_password)
|
||||
self._assert_response(response, success=False)
|
||||
|
||||
def _login_response(self, email, password):
|
||||
post_params = {'email': email, 'password': password}
|
||||
return self.client.post(self.url, post_params)
|
||||
|
||||
def _assert_response(self, response, success=None, value=None):
|
||||
'''
|
||||
Assert that the response had status 200 and returned a valid
|
||||
JSON-parseable dict.
|
||||
|
||||
If success is provided, assert that the response had that
|
||||
value for 'success' in the JSON dict.
|
||||
|
||||
If value is provided, assert that the response contained that
|
||||
value for 'value' in the JSON dict.
|
||||
'''
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
try:
|
||||
response_dict = json.loads(response.content)
|
||||
except ValueError:
|
||||
self.fail("Could not parse response content as JSON: %s"
|
||||
% str(response.content))
|
||||
|
||||
if success is not None:
|
||||
self.assertEqual(response_dict['success'], success)
|
||||
|
||||
if value is not None:
|
||||
msg = ("'%s' did not contain '%s'" %
|
||||
(str(response_dict['value']), str(value)))
|
||||
self.assertTrue(value in response_dict['value'], msg)
|
||||
@@ -32,9 +32,9 @@ course_id = 'edX/test_course/test'
|
||||
|
||||
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
|
||||
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
|
||||
student_state_key = partial(LmsKeyValueStore.Key, Scope.student_state, 'user', location('def_id'))
|
||||
student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem')
|
||||
student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None)
|
||||
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
|
||||
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
|
||||
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
|
||||
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
@@ -115,13 +115,13 @@ class TestInvalidScopes(TestCase):
|
||||
def setUp(self):
|
||||
self.desc_md = {}
|
||||
self.user = UserFactory.create()
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
def test_invalid_scopes(self):
|
||||
for scope in (Scope(student=True, block=BlockScope.DEFINITION),
|
||||
Scope(student=False, block=BlockScope.TYPE),
|
||||
Scope(student=False, block=BlockScope.ALL)):
|
||||
for scope in (Scope(user=True, block=BlockScope.DEFINITION),
|
||||
Scope(user=False, block=BlockScope.TYPE),
|
||||
Scope(user=False, block=BlockScope.ALL)):
|
||||
self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
|
||||
self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
|
||||
self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
|
||||
@@ -134,48 +134,48 @@ class TestStudentModuleStorage(TestCase):
|
||||
self.desc_md = {}
|
||||
student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
|
||||
self.user = student_module.student
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user)
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
def test_get_existing_field(self):
|
||||
"Test that getting an existing field in an existing StudentModule works"
|
||||
self.assertEquals('a_value', self.kvs.get(student_state_key('a_field')))
|
||||
self.assertEquals('a_value', self.kvs.get(user_state_key('a_field')))
|
||||
|
||||
def test_get_missing_field(self):
|
||||
"Test that getting a missing field from an existing StudentModule raises a KeyError"
|
||||
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
|
||||
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
|
||||
|
||||
def test_set_existing_field(self):
|
||||
"Test that setting an existing student_state field changes the value"
|
||||
self.kvs.set(student_state_key('a_field'), 'new_value')
|
||||
"Test that setting an existing user_state field changes the value"
|
||||
self.kvs.set(user_state_key('a_field'), 'new_value')
|
||||
self.assertEquals(1, StudentModule.objects.all().count())
|
||||
self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
|
||||
|
||||
def test_set_missing_field(self):
|
||||
"Test that setting a new student_state field changes the value"
|
||||
self.kvs.set(student_state_key('not_a_field'), 'new_value')
|
||||
"Test that setting a new user_state field changes the value"
|
||||
self.kvs.set(user_state_key('not_a_field'), 'new_value')
|
||||
self.assertEquals(1, StudentModule.objects.all().count())
|
||||
self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
|
||||
|
||||
def test_delete_existing_field(self):
|
||||
"Test that deleting an existing field removes it from the StudentModule"
|
||||
self.kvs.delete(student_state_key('a_field'))
|
||||
self.kvs.delete(user_state_key('a_field'))
|
||||
self.assertEquals(1, StudentModule.objects.all().count())
|
||||
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field'))
|
||||
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
|
||||
|
||||
def test_delete_missing_field(self):
|
||||
"Test that deleting a missing field from an existing StudentModule raises a KeyError"
|
||||
self.assertRaises(KeyError, self.kvs.delete, student_state_key('not_a_field'))
|
||||
self.assertRaises(KeyError, self.kvs.delete, user_state_key('not_a_field'))
|
||||
self.assertEquals(1, StudentModule.objects.all().count())
|
||||
self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
|
||||
|
||||
def test_has_existing_field(self):
|
||||
"Test that `has` returns True for existing fields in StudentModules"
|
||||
self.assertTrue(self.kvs.has(student_state_key('a_field')))
|
||||
self.assertTrue(self.kvs.has(user_state_key('a_field')))
|
||||
|
||||
def test_has_missing_field(self):
|
||||
"Test that `has` returns False for missing fields in StudentModule"
|
||||
self.assertFalse(self.kvs.has(student_state_key('not_a_field')))
|
||||
self.assertFalse(self.kvs.has(user_state_key('not_a_field')))
|
||||
|
||||
|
||||
class TestMissingStudentModule(TestCase):
|
||||
@@ -187,14 +187,14 @@ class TestMissingStudentModule(TestCase):
|
||||
|
||||
def test_get_field_from_missing_student_module(self):
|
||||
"Test that getting a field from a missing StudentModule raises a KeyError"
|
||||
self.assertRaises(KeyError, self.kvs.get, student_state_key('a_field'))
|
||||
self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field'))
|
||||
|
||||
def test_set_field_in_missing_student_module(self):
|
||||
"Test that setting a field in a missing StudentModule creates the student module"
|
||||
self.assertEquals(0, len(self.mdc.cache))
|
||||
self.assertEquals(0, StudentModule.objects.all().count())
|
||||
|
||||
self.kvs.set(student_state_key('a_field'), 'a_value')
|
||||
self.kvs.set(user_state_key('a_field'), 'a_value')
|
||||
|
||||
self.assertEquals(1, len(self.mdc.cache))
|
||||
self.assertEquals(1, StudentModule.objects.all().count())
|
||||
@@ -207,11 +207,11 @@ class TestMissingStudentModule(TestCase):
|
||||
|
||||
def test_delete_field_from_missing_student_module(self):
|
||||
"Test that deleting a field from a missing StudentModule raises a KeyError"
|
||||
self.assertRaises(KeyError, self.kvs.delete, student_state_key('a_field'))
|
||||
self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field'))
|
||||
|
||||
def test_has_field_for_missing_student_module(self):
|
||||
"Test that `has` returns False for missing StudentModules"
|
||||
self.assertFalse(self.kvs.has(student_state_key('a_field')))
|
||||
self.assertFalse(self.kvs.has(user_state_key('a_field')))
|
||||
|
||||
|
||||
class StorageTestBase(object):
|
||||
@@ -286,13 +286,13 @@ class TestContentStorage(StorageTestBase, TestCase):
|
||||
|
||||
class TestStudentPrefsStorage(StorageTestBase, TestCase):
|
||||
factory = StudentPrefsFactory
|
||||
scope = Scope.student_preferences
|
||||
key_factory = student_prefs_key
|
||||
scope = Scope.preferences
|
||||
key_factory = prefs_key
|
||||
storage_class = XModuleStudentPrefsField
|
||||
|
||||
|
||||
class TestStudentInfoStorage(StorageTestBase, TestCase):
|
||||
factory = StudentInfoFactory
|
||||
scope = Scope.student_info
|
||||
key_factory = student_info_key
|
||||
scope = Scope.user_info
|
||||
key_factory = user_info_key
|
||||
storage_class = XModuleStudentInfoField
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import logging
|
||||
from mock import MagicMock, patch
|
||||
from mock import MagicMock
|
||||
import json
|
||||
import factory
|
||||
import unittest
|
||||
from nose.tools import set_trace
|
||||
|
||||
from django.http import Http404, HttpResponse, HttpRequest
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import courseware.module_render as render
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.seq_module import SequenceModule
|
||||
from courseware.tests.tests import PageLoader
|
||||
from student.models import Registration
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase
|
||||
from courseware.model_data import ModelDataCache
|
||||
|
||||
from .factories import UserFactory
|
||||
@@ -49,10 +38,9 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class ModuleRenderTestCase(PageLoader):
|
||||
class ModuleRenderTestCase(LoginEnrollmentTestCase):
|
||||
def setUp(self):
|
||||
self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
|
||||
self._MODULESTORES = {}
|
||||
self.course_id = 'edX/toy/2012_Fall'
|
||||
self.toy_course = modulestore().get_course(self.course_id)
|
||||
|
||||
@@ -66,10 +54,9 @@ class ModuleRenderTestCase(PageLoader):
|
||||
mock_request = MagicMock()
|
||||
mock_request.FILES.keys.return_value = ['file_id']
|
||||
mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)
|
||||
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location,
|
||||
'dummy').content,
|
||||
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
|
||||
settings.MAX_FILEUPLOADS_PER_INPUT}))
|
||||
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, 'dummy').content,
|
||||
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
|
||||
settings.MAX_FILEUPLOADS_PER_INPUT}))
|
||||
mock_request_2 = MagicMock()
|
||||
mock_request_2.FILES.keys.return_value = ['file_id']
|
||||
inputfile = Stub()
|
||||
@@ -80,7 +67,7 @@ class ModuleRenderTestCase(PageLoader):
|
||||
self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location,
|
||||
'dummy').content,
|
||||
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))}))
|
||||
mock_request_3 = MagicMock()
|
||||
mock_request_3.POST.copy.return_value = {}
|
||||
mock_request_3.FILES = False
|
||||
@@ -91,10 +78,10 @@ class ModuleRenderTestCase(PageLoader):
|
||||
self.assertRaises(ItemNotFoundError, render.modx_dispatch,
|
||||
mock_request_3, 'dummy', self.location, 'toy')
|
||||
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy',
|
||||
self.location, self.course_id)
|
||||
self.location, self.course_id)
|
||||
mock_request_3.POST.copy.return_value = {'position': 1}
|
||||
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position',
|
||||
self.location, self.course_id), HttpResponse)
|
||||
self.location, self.course_id), HttpResponse)
|
||||
|
||||
def test_get_score_bucket(self):
|
||||
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
|
||||
@@ -104,12 +91,23 @@ class ModuleRenderTestCase(PageLoader):
|
||||
self.assertEquals(render.get_score_bucket(11, 10), 'incorrect')
|
||||
self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect')
|
||||
|
||||
def test_anonymous_modx_dispatch(self):
|
||||
dispatch_url = reverse(
|
||||
'modx_dispatch',
|
||||
args=[
|
||||
'edX/toy/2012_Fall',
|
||||
'i4x://edX/toy/videosequence/Toy_Videos',
|
||||
'goto_position'
|
||||
]
|
||||
)
|
||||
response = self.client.post(dispatch_url, {'position': 2})
|
||||
self.assertEquals(403, response.status_code)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestTOC(TestCase):
|
||||
"""Check the Table of Contents for a course"""
|
||||
def setUp(self):
|
||||
self._MODULESTORES = {}
|
||||
|
||||
# Toy courses should be loaded
|
||||
self.course_name = 'edX/toy/2012_Fall'
|
||||
@@ -125,19 +123,19 @@ class TestTOC(TestCase):
|
||||
self.toy_course.id, self.portal_user, self.toy_course, depth=2)
|
||||
|
||||
expected = ([{'active': True, 'sections':
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': '', 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': None, 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
|
||||
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -152,19 +150,19 @@ class TestTOC(TestCase):
|
||||
self.toy_course.id, self.portal_user, self.toy_course, depth=2)
|
||||
|
||||
expected = ([{'active': True, 'sections':
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': '', 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': '', 'active': True},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': '', 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
|
||||
'format': u'Lecture Sequence', 'due': None, 'active': False},
|
||||
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
|
||||
'format': '', 'due': None, 'active': True},
|
||||
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False},
|
||||
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'Overview', 'display_name': u'Overview'},
|
||||
{'active': False, 'sections':
|
||||
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
|
||||
'format': '', 'due': None, 'active': False}],
|
||||
'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
|
||||
|
||||
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import logging
|
||||
from mock import MagicMock, patch
|
||||
from mock import MagicMock
|
||||
import datetime
|
||||
import factory
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from django.test import TestCase
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError,\
|
||||
ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
import courseware.views as views
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
class Stub():
|
||||
pass
|
||||
@@ -55,7 +48,6 @@ class TestJumpTo(TestCase):
|
||||
def test_jumpto_invalid_location(self):
|
||||
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
|
||||
jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location)
|
||||
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
|
||||
response = self.client.get(jumpto_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@@ -124,3 +116,26 @@ class ViewsTestCase(TestCase):
|
||||
request, 'bar', ())
|
||||
self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request,
|
||||
'dummy', self.location)
|
||||
|
||||
def test_no_end_on_about_page(self):
|
||||
# Toy course has no course end date or about/end_date blob
|
||||
self.verify_end_date(self.course_id)
|
||||
|
||||
def test_no_end_about_blob(self):
|
||||
# test_end has a course end date, no end_date HTML blob
|
||||
self.verify_end_date("edX/test_end/2012_Fall", "Sep 17, 2015")
|
||||
|
||||
def test_about_blob_end_date(self):
|
||||
# test_about_blob_end_date has both a course end date and an end_date HTML blob.
|
||||
# HTML blob wins
|
||||
self.verify_end_date("edX/test_about_blob_end_date/2012_Fall", "Learning never ends")
|
||||
|
||||
def verify_end_date(self, course_id, expected_end_text=None):
|
||||
request = self.request_factory.get("foo")
|
||||
request.user = self.user
|
||||
result = views.course_about(request, course_id)
|
||||
if expected_end_text is not None:
|
||||
self.assertContains(result, "Classes End")
|
||||
self.assertContains(result, expected_end_text)
|
||||
else:
|
||||
self.assertNotContains(result, "Classes End")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import logging
|
||||
log = logging.getLogger("mitx." + __name__)
|
||||
'''
|
||||
Test for lms courseware app
|
||||
'''
|
||||
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
|
||||
@@ -14,8 +17,6 @@ from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from courseware import grades
|
||||
@@ -29,7 +30,8 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
log = logging.getLogger("mitx." + __name__)
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
@@ -37,21 +39,22 @@ def parse_json(response):
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
def user(email):
|
||||
def get_user(email):
|
||||
'''look up a user by email'''
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
def get_registration(email):
|
||||
'''look up registration object by email'''
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
# A bit of a hack--want mongo modulestore for these tests, until
|
||||
# jump_to works with the xmlmodulestore or we have an even better solution
|
||||
# NOTE: this means this test requires mongo to be running.
|
||||
|
||||
|
||||
def mongo_store_config(data_dir):
|
||||
'''
|
||||
Defines default module store using MongoModuleStore
|
||||
|
||||
Use of this config requires mongo to be running
|
||||
'''
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
@@ -68,6 +71,7 @@ def mongo_store_config(data_dir):
|
||||
|
||||
|
||||
def draft_mongo_store_config(data_dir):
|
||||
'''Defines default module store using DraftMongoModuleStore'''
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
@@ -84,6 +88,7 @@ def draft_mongo_store_config(data_dir):
|
||||
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
'''Defines default module store using XMLModuleStore'''
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
@@ -100,8 +105,11 @@ TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
|
||||
class ActivateLoginTestCase(TestCase):
|
||||
'''Check that we can activate and log in'''
|
||||
class LoginEnrollmentTestCase(TestCase):
|
||||
'''
|
||||
Base TestCase providing support for user creation,
|
||||
activation, login, and course enrollment
|
||||
'''
|
||||
|
||||
def assertRedirectsNoFollow(self, response, expected_url):
|
||||
"""
|
||||
@@ -112,37 +120,42 @@ class ActivateLoginTestCase(TestCase):
|
||||
Some of the code taken from django.test.testcases.py
|
||||
"""
|
||||
self.assertEqual(response.status_code, 302,
|
||||
'Response status code was {0} instead of 302'.format(response.status_code))
|
||||
'Response status code was %d instead of 302'
|
||||
% (response.status_code))
|
||||
url = response['Location']
|
||||
|
||||
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
|
||||
if not (e_scheme or e_netloc):
|
||||
expected_url = urlunsplit(('http', 'testserver', e_path,
|
||||
e_query, e_fragment))
|
||||
expected_url = urlunsplit(('http', 'testserver',
|
||||
e_path, e_query, e_fragment))
|
||||
|
||||
self.assertEqual(url, expected_url, "Response redirected to '{0}', expected '{1}'".format(
|
||||
url, expected_url))
|
||||
self.assertEqual(url, expected_url,
|
||||
"Response redirected to '%s', expected '%s'" %
|
||||
(url, expected_url))
|
||||
|
||||
def setUp(self):
|
||||
email = 'view@test.com'
|
||||
password = 'foo'
|
||||
self.create_account('viewtest', email, password)
|
||||
self.activate_user(email)
|
||||
self.login(email, password)
|
||||
def setup_viewtest_user(self):
|
||||
'''create a user account, activate, and log in'''
|
||||
self.viewtest_email = 'view@test.com'
|
||||
self.viewtest_password = 'foo'
|
||||
self.viewtest_username = 'viewtest'
|
||||
self.create_account(self.viewtest_username,
|
||||
self.viewtest_email, self.viewtest_password)
|
||||
self.activate_user(self.viewtest_email)
|
||||
self.login(self.viewtest_email, self.viewtest_password)
|
||||
|
||||
# ============ User creation and login ==============
|
||||
|
||||
def _login(self, email, pw):
|
||||
def _login(self, email, password):
|
||||
'''Login. View should always return 200. The success/fail is in the
|
||||
returned json'''
|
||||
resp = self.client.post(reverse('login'),
|
||||
{'email': email, 'password': pw})
|
||||
{'email': email, 'password': password})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp
|
||||
|
||||
def login(self, email, pw):
|
||||
def login(self, email, password):
|
||||
'''Login, check that it worked.'''
|
||||
resp = self._login(email, pw)
|
||||
resp = self._login(email, password)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
@@ -154,56 +167,45 @@ class ActivateLoginTestCase(TestCase):
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
return resp
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
def _create_account(self, username, email, password):
|
||||
'''Try to create an account. No error checking'''
|
||||
resp = self.client.post('/create_account', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': pw,
|
||||
'password': password,
|
||||
'name': 'Fred Weasley',
|
||||
'terms_of_service': 'true',
|
||||
'honor_code': 'true',
|
||||
})
|
||||
return resp
|
||||
|
||||
def create_account(self, username, email, pw):
|
||||
def create_account(self, username, email, password):
|
||||
'''Create the account and check that it worked'''
|
||||
resp = self._create_account(username, email, pw)
|
||||
resp = self._create_account(username, email, password)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# Check both that the user is created, and inactive
|
||||
self.assertFalse(user(email).is_active)
|
||||
self.assertFalse(get_user(email).is_active)
|
||||
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''Look up the activation key for the user, then hit the activate view.
|
||||
No error checking'''
|
||||
activation_key = registration(email).activation_key
|
||||
activation_key = get_registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
||||
url = reverse('activate', kwargs={'key': activation_key})
|
||||
resp = self.client.get(url)
|
||||
return resp
|
||||
|
||||
def activate_user(self, email):
|
||||
resp = self._activate_user(email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
def test_activate_login(self):
|
||||
'''The setup function does all the work'''
|
||||
pass
|
||||
|
||||
def test_logout(self):
|
||||
'''Setup function does login'''
|
||||
self.logout()
|
||||
|
||||
|
||||
class PageLoader(ActivateLoginTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
self.assertTrue(get_user(email).is_active)
|
||||
|
||||
def _enroll(self, course):
|
||||
"""Post to the enrollment view, and return the parsed json response"""
|
||||
@@ -216,7 +218,8 @@ class PageLoader(ActivateLoginTestCase):
|
||||
def try_enroll(self, course):
|
||||
"""Try to enroll. Return bool success instead of asserting it."""
|
||||
data = self._enroll(course)
|
||||
print 'Enrollment in {0} result: {1}'.format(course.location.url(), data)
|
||||
print ('Enrollment in %s result: %s'
|
||||
% (course.location.url(), str(data)))
|
||||
return data['success']
|
||||
|
||||
def enroll(self, course):
|
||||
@@ -240,8 +243,8 @@ class PageLoader(ActivateLoginTestCase):
|
||||
"""
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
"got code %d for url '%s'. Expected code %d"
|
||||
% (resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
def check_for_post_code(self, code, url, data={}):
|
||||
@@ -251,12 +254,32 @@ class PageLoader(ActivateLoginTestCase):
|
||||
"""
|
||||
resp = self.client.post(url, data)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
"got code %d for url '%s'. Expected code %d"
|
||||
% (resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
def check_pages_load(self, module_store):
|
||||
"""Make all locations in course load"""
|
||||
|
||||
class ActivateLoginTest(LoginEnrollmentTestCase):
|
||||
'''Test logging in and logging out'''
|
||||
def setUp(self):
|
||||
self.setup_viewtest_user()
|
||||
|
||||
def test_activate_login(self):
|
||||
'''Test login -- the setup function does all the work'''
|
||||
pass
|
||||
|
||||
def test_logout(self):
|
||||
'''Test logout -- setup function does login'''
|
||||
self.logout()
|
||||
|
||||
|
||||
class PageLoaderTestCase(LoginEnrollmentTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
def check_random_page_loads(self, module_store):
|
||||
'''
|
||||
Choose a page in the course randomly, and assert that it loads
|
||||
'''
|
||||
# enroll in the course before trying to access pages
|
||||
courses = module_store.get_courses()
|
||||
self.assertEqual(len(courses), 1)
|
||||
@@ -264,129 +287,108 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.enroll(course)
|
||||
course_id = course.id
|
||||
|
||||
n = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
# Search for items in the course
|
||||
# None is treated as a wildcard
|
||||
course_loc = course.location
|
||||
location_query = Location(course_loc.tag, course_loc.org,
|
||||
course_loc.course, None, None, None)
|
||||
|
||||
for descriptor in module_store.get_items(
|
||||
Location(None, None, None, None, None)):
|
||||
items = module_store.get_items(location_query)
|
||||
|
||||
n += 1
|
||||
print "Checking ", descriptor.location.url()
|
||||
if len(items) < 1:
|
||||
self.fail('Could not retrieve any items from course')
|
||||
else:
|
||||
descriptor = random.choice(items)
|
||||
|
||||
# We have ancillary course information now as modules and we can't simply use 'jump_to' to view them
|
||||
if descriptor.location.category == 'about':
|
||||
resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
# We have ancillary course information now as modules
|
||||
# and we can't simply use 'jump_to' to view them
|
||||
if descriptor.location.category == 'about':
|
||||
self._assert_loads('about_course',
|
||||
{'course_id': course_id},
|
||||
descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'static_tab':
|
||||
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug': descriptor.location.name}))
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'static_tab':
|
||||
kwargs = {'course_id': course_id,
|
||||
'tab_slug': descriptor.location.name}
|
||||
self._assert_loads('static_tab', kwargs, descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'course_info':
|
||||
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'course_info':
|
||||
self._assert_loads('info', {'course_id': course_id},
|
||||
descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'custom_tag_template':
|
||||
pass
|
||||
else:
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'course_id': course_id,
|
||||
'location': descriptor.location.url()}), follow=True)
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'custom_tag_template':
|
||||
pass
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg + ": " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif resp.redirect_chain[0][1] != 302:
|
||||
msg = "ERROR on redirect from " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
else:
|
||||
|
||||
# check content to make sure there were no rendering failures
|
||||
content = resp.content
|
||||
if content.find("this module is temporarily unavailable") >= 0:
|
||||
msg = "ERROR unavailable module "
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif isinstance(descriptor, ErrorDescriptor):
|
||||
msg = "ERROR error descriptor loaded: "
|
||||
msg = msg + descriptor.error_msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
kwargs = {'course_id': course_id,
|
||||
'location': descriptor.location.url()}
|
||||
|
||||
print msg
|
||||
self.assertTrue(all_ok) # fail fast
|
||||
self._assert_loads('jump_to', kwargs, descriptor,
|
||||
expect_redirect=True,
|
||||
check_content=True)
|
||||
|
||||
print "{0}/{1} good".format(n - num_bad, n)
|
||||
log.info("{0}/{1} good".format(n - num_bad, n))
|
||||
self.assertTrue(all_ok)
|
||||
def _assert_loads(self, django_url, kwargs, descriptor,
|
||||
expect_redirect=False,
|
||||
check_content=False):
|
||||
'''
|
||||
Assert that the url loads correctly.
|
||||
If expect_redirect, then also check that we were redirected.
|
||||
If check_content, then check that we don't get
|
||||
an error message about unavailable modules.
|
||||
'''
|
||||
|
||||
url = reverse(django_url, kwargs=kwargs)
|
||||
response = self.client.get(url, follow=True)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.fail('Status %d for page %s' %
|
||||
(response.status_code, descriptor.location.url()))
|
||||
|
||||
if expect_redirect:
|
||||
self.assertEqual(response.redirect_chain[0][1], 302)
|
||||
|
||||
if check_content:
|
||||
unavailable_msg = "this module is temporarily unavailable"
|
||||
self.assertEqual(response.content.find(unavailable_msg), -1)
|
||||
self.assertFalse(isinstance(descriptor, ErrorDescriptor))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestCoursesLoadTestCase_XmlModulestore(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase):
|
||||
'''Check that all pages in test courses load properly from XML'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
self.setup_viewtest_user()
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
module_store = XMLModuleStore(
|
||||
TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
course_dirs=['toy'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
module_class = 'xmodule.hidden_module.HiddenDescriptor'
|
||||
module_store = XMLModuleStore(TEST_DATA_DIR,
|
||||
default_class=module_class,
|
||||
course_dirs=['toy'],
|
||||
load_error_modules=True)
|
||||
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
module_store = XMLModuleStore(
|
||||
TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
course_dirs=['full'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
self.check_pages_load(module_store)
|
||||
self.check_random_page_loads(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestCoursesLoadTestCase_MongoModulestore(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
|
||||
'''Check that all pages in test courses load properly from Mongo'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
self.setup_viewtest_user()
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['full'])
|
||||
self.check_pages_load(module_store)
|
||||
self.check_random_page_loads(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestNavigation(PageLoader):
|
||||
class TestNavigation(LoginEnrollmentTestCase):
|
||||
"""Check that navigation state is saved properly"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -412,42 +414,56 @@ class TestNavigation(PageLoader):
|
||||
self.enroll(self.full)
|
||||
|
||||
# First request should redirect to ToyVideos
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
# Don't use no-follow, because state should only be saved once we actually hit the section
|
||||
# Don't use no-follow, because state should
|
||||
# only be saved once we actually hit the section
|
||||
self.assertRedirects(resp, reverse(
|
||||
'courseware_section', kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
|
||||
# Hitting the couseware tab again should redirect to the first chapter: 'Overview'
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
# Hitting the couseware tab again should
|
||||
# redirect to the first chapter: 'Overview'
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
||||
kwargs={'course_id': self.toy.id, 'chapter': 'Overview'}))
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview'}))
|
||||
|
||||
# Now we directly navigate to a section in a different chapter
|
||||
self.check_for_get_code(200, reverse('courseware_section',
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'secret:magic', 'section': 'toyvideo'}))
|
||||
'chapter': 'secret:magic',
|
||||
'section': 'toyvideo'}))
|
||||
|
||||
# And now hitting the courseware tab should redirect to 'secret:magic'
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
||||
kwargs={'course_id': self.toy.id, 'chapter': 'secret:magic'}))
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'secret:magic'}))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE)
|
||||
class TestDraftModuleStore(TestCase):
|
||||
def test_get_items_with_course_items(self):
|
||||
store = modulestore()
|
||||
|
||||
# fix was to allow get_items() to take the course_id parameter
|
||||
store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0)
|
||||
# test success is just getting through the above statement. The bug was that 'course_id' argument was
|
||||
store.get_items(Location(None, None, 'vertical', None, None),
|
||||
course_id='abc', depth=0)
|
||||
|
||||
# test success is just getting through the above statement.
|
||||
# The bug was that 'course_id' argument was
|
||||
# not allowed to be passed in (i.e. was throwing exception)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestViewAuth(PageLoader):
|
||||
class TestViewAuth(LoginEnrollmentTestCase):
|
||||
"""Check that view authentication works properly"""
|
||||
|
||||
# NOTE: setUpClass() runs before override_settings takes effect, so
|
||||
@@ -469,21 +485,29 @@ class TestViewAuth(PageLoader):
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
def test_instructor_pages(self):
|
||||
"""Make sure only instructors for the course or staff can load the instructor
|
||||
"""Make sure only instructors for the course
|
||||
or staff can load the instructor
|
||||
dashboard, the grade views, and student profile pages"""
|
||||
|
||||
# First, try with an enrolled student
|
||||
self.login(self.student, self.password)
|
||||
# shouldn't work before enroll
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response, reverse('about_course', args=[self.toy.id]))
|
||||
response = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(response,
|
||||
reverse('about_course',
|
||||
args=[self.toy.id]))
|
||||
self.enroll(self.toy)
|
||||
self.enroll(self.full)
|
||||
# should work now -- redirect to first page
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response, reverse('courseware_section', kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
response = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response,
|
||||
reverse('courseware_section',
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
|
||||
def instructor_urls(course):
|
||||
"list of urls that only instructors/staff should be able to see"
|
||||
@@ -491,41 +515,47 @@ class TestViewAuth(PageLoader):
|
||||
'instructor_dashboard',
|
||||
'gradebook',
|
||||
'grade_summary',)]
|
||||
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
|
||||
'student_id': user(self.student).id}))
|
||||
|
||||
urls.append(reverse('student_progress',
|
||||
kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id}))
|
||||
return urls
|
||||
|
||||
# shouldn't be able to get to the instructor pages
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
# Randomly sample an instructor page
|
||||
url = random.choice(instructor_urls(self.toy) +
|
||||
instructor_urls(self.full))
|
||||
|
||||
# Shouldn't be able to get to the instructor pages
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
group = Group.objects.create(name=group_name)
|
||||
group.user_set.add(get_user(self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
# Now should be able to get to the toy course, but not the full course
|
||||
for url in instructor_urls(self.toy):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
url = random.choice(instructor_urls(self.toy))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
for url in instructor_urls(self.full):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
url = random.choice(instructor_urls(self.full))
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# now also make the instructor staff
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
instructor = get_user(self.instructor)
|
||||
instructor.is_staff = True
|
||||
instructor.save()
|
||||
|
||||
# and now should be able to load both
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
url = random.choice(instructor_urls(self.toy) +
|
||||
instructor_urls(self.full))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
@@ -569,7 +599,8 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
|
||||
return [reverse(name, kwargs={'course_id': course.id})
|
||||
for name in names]
|
||||
|
||||
def dark_student_urls(course):
|
||||
"""
|
||||
@@ -578,7 +609,8 @@ class TestViewAuth(PageLoader):
|
||||
"""
|
||||
urls = reverse_urls(['info', 'progress'], course)
|
||||
urls.extend([
|
||||
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
|
||||
reverse('book', kwargs={'course_id': course.id,
|
||||
'book_index': book.title})
|
||||
for book in course.textbooks
|
||||
])
|
||||
return urls
|
||||
@@ -597,37 +629,46 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
def instructor_urls(course):
|
||||
"""list of urls that only instructors/staff should be able to see"""
|
||||
urls = reverse_urls(['instructor_dashboard', 'gradebook', 'grade_summary'],
|
||||
course)
|
||||
urls = reverse_urls(['instructor_dashboard',
|
||||
'gradebook', 'grade_summary'], course)
|
||||
return urls
|
||||
|
||||
def check_non_staff(course):
|
||||
"""Check that access is right for non-staff in course"""
|
||||
print '=== Checking non-staff access for {0}'.format(course.id)
|
||||
for url in instructor_urls(course) + dark_student_urls(course) + reverse_urls(['courseware'], course):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
for url in light_student_urls(course):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
# Randomly sample a dark url
|
||||
url = random.choice(instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
reverse_urls(['courseware'], course))
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Randomly sample a light url
|
||||
url = random.choice(light_student_urls(course))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
def check_staff(course):
|
||||
"""Check that access is right for staff in course"""
|
||||
print '=== Checking staff access for {0}'.format(course.id)
|
||||
for url in (instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
light_student_urls(course)):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
# Randomly sample a url
|
||||
url = random.choice(instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
light_student_urls(course))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
# The student progress tab is not accessible to a student
|
||||
# before launch, so the instructor view-as-student feature should return a 404 as well.
|
||||
# before launch, so the instructor view-as-student feature
|
||||
# should return a 404 as well.
|
||||
# TODO (vshnayder): If this is not the behavior we want, will need
|
||||
# to make access checking smarter and understand both the effective
|
||||
# user (the student), and the requesting user (the prof)
|
||||
url = reverse('student_progress', kwargs={'course_id': course.id,
|
||||
'student_id': user(self.student).id})
|
||||
url = reverse('student_progress',
|
||||
kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id})
|
||||
print 'checking for 404 on view-as-student: {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
@@ -648,8 +689,8 @@ class TestViewAuth(PageLoader):
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
group = Group.objects.create(name=group_name)
|
||||
group.user_set.add(get_user(self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
@@ -663,9 +704,9 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing staff access....'
|
||||
# now also make the instructor staff
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
instructor = get_user(self.instructor)
|
||||
instructor.is_staff = True
|
||||
instructor.save()
|
||||
|
||||
# and now should be able to load both
|
||||
check_staff(self.toy)
|
||||
@@ -698,8 +739,8 @@ class TestViewAuth(PageLoader):
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
group = Group.objects.create(name=group_name)
|
||||
group.user_set.add(get_user(self.instructor))
|
||||
|
||||
print "logout/login"
|
||||
self.logout()
|
||||
@@ -709,10 +750,10 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
print '=== Testing staff access....'
|
||||
# now make the instructor global staff, but not in the instructor group
|
||||
g.user_set.remove(user(self.instructor))
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
group.user_set.remove(get_user(self.instructor))
|
||||
instructor = get_user(self.instructor)
|
||||
instructor.is_staff = True
|
||||
instructor.save()
|
||||
|
||||
# unenroll and try again
|
||||
self.unenroll(self.toy)
|
||||
@@ -726,8 +767,8 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24 * 3600
|
||||
nextday = tomorrow + 24 * 3600
|
||||
yesterday = time.time() - 24 * 3600
|
||||
# nextday = tomorrow + 24 * 3600
|
||||
# yesterday = time.time() - 24 * 3600
|
||||
|
||||
# toy course's hasn't started
|
||||
self.toy.lms.start = time.gmtime(tomorrow)
|
||||
@@ -737,20 +778,20 @@ class TestViewAuth(PageLoader):
|
||||
self.toy.lms.days_early_for_beta = 2
|
||||
|
||||
# student user shouldn't see it
|
||||
student_user = user(self.student)
|
||||
student_user = get_user(self.student)
|
||||
self.assertFalse(has_access(student_user, self.toy, 'load'))
|
||||
|
||||
# now add the student to the beta test group
|
||||
group_name = course_beta_test_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(student_user)
|
||||
group = Group.objects.create(name=group_name)
|
||||
group.user_set.add(student_user)
|
||||
|
||||
# now the student should see it
|
||||
self.assertTrue(has_access(student_user, self.toy, 'load'))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestCourseGrader(PageLoader):
|
||||
class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
"""Check that a course gets graded properly"""
|
||||
|
||||
# NOTE: setUpClass() runs before override_settings takes effect, so
|
||||
@@ -773,35 +814,41 @@ class TestCourseGrader(PageLoader):
|
||||
self.activate_user(self.student)
|
||||
self.enroll(self.graded_course)
|
||||
|
||||
self.student_user = user(self.student)
|
||||
self.student_user = get_user(self.student)
|
||||
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def get_grade_summary(self):
|
||||
'''calls grades.grade for current user and course'''
|
||||
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, model_data_cache)
|
||||
|
||||
def get_homework_scores(self):
|
||||
'''get scores for homeworks'''
|
||||
return self.get_grade_summary()['totaled_scores']['Homework']
|
||||
|
||||
def get_progress_summary(self):
|
||||
'''return progress summary structure for current user and course'''
|
||||
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
self.graded_course, model_data_cache)
|
||||
progress_summary = grades.progress_summary(self.student_user,
|
||||
fake_request,
|
||||
self.graded_course,
|
||||
model_data_cache)
|
||||
return progress_summary
|
||||
|
||||
def check_grade_percent(self, percent):
|
||||
'''assert that percent grade is as expected'''
|
||||
grade_summary = self.get_grade_summary()
|
||||
self.assertEqual(grade_summary['percent'], percent)
|
||||
|
||||
@@ -813,34 +860,34 @@ class TestCourseGrader(PageLoader):
|
||||
input_i4x-edX-graded-problem-H1P3_2_1
|
||||
input_i4x-edX-graded-problem-H1P3_2_2
|
||||
"""
|
||||
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
problem_location = "i4x://edX/graded/problem/%s" % problem_url_name
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_check', })
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_check', })
|
||||
|
||||
resp = self.client.post(modx_url, {
|
||||
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
|
||||
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
|
||||
})
|
||||
'input_i4x-edX-graded-problem-%s_2_1' % problem_url_name: responses[0],
|
||||
'input_i4x-edX-graded-problem-%s_2_2' % problem_url_name: responses[1],
|
||||
})
|
||||
print "modx_url", modx_url, "responses", responses
|
||||
print "resp", resp
|
||||
|
||||
return resp
|
||||
|
||||
def problem_location(self, problem_url_name):
|
||||
'''Get location string for problem, assuming hardcoded course_id'''
|
||||
return "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
|
||||
def reset_question_answer(self, problem_url_name):
|
||||
'''resets specified problem for current user'''
|
||||
problem_location = self.problem_location(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_reset', })
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_reset', })
|
||||
|
||||
resp = self.client.post(modx_url)
|
||||
return resp
|
||||
@@ -855,6 +902,7 @@ class TestCourseGrader(PageLoader):
|
||||
return [s.earned for s in self.get_homework_scores()]
|
||||
|
||||
def score_for_hw(hw_url_name):
|
||||
"""returns list of scores for a given url"""
|
||||
hw_section = [section for section
|
||||
in self.get_progress_summary()[0]['sections']
|
||||
if section.get('url_name') == hw_url_name][0]
|
||||
@@ -879,7 +927,8 @@ class TestCourseGrader(PageLoader):
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
|
||||
# This problem is hidden in an ABTest.
|
||||
# Getting it correct doesn't change total grade
|
||||
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
#from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
|
||||
@@ -67,9 +66,9 @@ def user_groups(user):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def courses(request):
|
||||
'''
|
||||
"""
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
'''
|
||||
"""
|
||||
courses = get_courses(request.user, request.META.get('HTTP_HOST'))
|
||||
courses = sort_by_announcement(courses)
|
||||
|
||||
@@ -77,14 +76,16 @@ def courses(request):
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section, model_data_cache):
|
||||
''' Draws navigation bar. Takes current position in accordion as
|
||||
parameter.
|
||||
"""
|
||||
Draws navigation bar. Takes current position in accordion as
|
||||
parameter.
|
||||
|
||||
If chapter and section are '' or None, renders a default accordion.
|
||||
If chapter and section are '' or None, renders a default accordion.
|
||||
|
||||
course, chapter, and section are the url_names.
|
||||
course, chapter, and section are the url_names.
|
||||
|
||||
Returns the html string'''
|
||||
Returns the html string
|
||||
"""
|
||||
|
||||
# grab the table of contents
|
||||
user = User.objects.prefetch_related("groups").get(id=request.user.id)
|
||||
@@ -92,7 +93,8 @@ def render_accordion(request, course, chapter, section, model_data_cache):
|
||||
|
||||
context = dict([('toc', toc),
|
||||
('course_id', course.id),
|
||||
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
|
||||
('csrf', csrf(request)['csrf_token']),
|
||||
('show_timezone', course.show_timezone)] + template_imports.items())
|
||||
return render_to_string('courseware/accordion.html', context)
|
||||
|
||||
|
||||
@@ -166,10 +168,10 @@ def save_child_position(seq_module, child_name):
|
||||
|
||||
|
||||
def check_for_active_timelimit_module(request, course_id, course):
|
||||
'''
|
||||
"""
|
||||
Looks for a timing module for the given user and course that is currently active.
|
||||
If found, returns a context dict with timer-related values to enable display of time remaining.
|
||||
'''
|
||||
"""
|
||||
context = {}
|
||||
|
||||
# TODO (cpennington): Once we can query the course structure, replace this with such a query
|
||||
@@ -201,11 +203,11 @@ def check_for_active_timelimit_module(request, course_id, course):
|
||||
|
||||
|
||||
def update_timelimit_module(user, course_id, model_data_cache, timelimit_descriptor, timelimit_module):
|
||||
'''
|
||||
"""
|
||||
Updates the state of the provided timing module, starting it if it hasn't begun.
|
||||
Returns dict with timer-related values to enable display of time remaining.
|
||||
Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired.
|
||||
'''
|
||||
"""
|
||||
context = {}
|
||||
# determine where to go when the exam ends:
|
||||
if timelimit_descriptor.time_expired_redirect_url is None:
|
||||
@@ -391,14 +393,14 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def jump_to(request, course_id, location):
|
||||
'''
|
||||
"""
|
||||
Show the page that contains a specific location.
|
||||
|
||||
If the location is invalid or not in any class, return a 404.
|
||||
|
||||
Otherwise, delegates to the index view to figure out whether this user
|
||||
has access, and what they should see.
|
||||
'''
|
||||
"""
|
||||
# Complain if the location isn't valid
|
||||
try:
|
||||
location = Location(location)
|
||||
@@ -486,7 +488,9 @@ def syllabus(request, course_id):
|
||||
|
||||
|
||||
def registered_for_course(course, user):
|
||||
'''Return CourseEnrollment if user is registered for course, else False'''
|
||||
"""
|
||||
Return CourseEnrollment if user is registered for course, else False
|
||||
"""
|
||||
if user is None:
|
||||
return False
|
||||
if user.is_authenticated():
|
||||
@@ -522,6 +526,12 @@ def static_university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id that does not have any courses.
|
||||
"""
|
||||
# Redirect to the properly capitalized org_id
|
||||
last_path = request.path.split('/')[-1]
|
||||
if last_path != org_id:
|
||||
return redirect('static_university_profile', org_id=org_id)
|
||||
|
||||
# Render template
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
context = dict(courses=[], org_id=org_id)
|
||||
return render_to_response(template_file, context)
|
||||
@@ -533,17 +543,28 @@ def university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id. 404 if it's not valid.
|
||||
"""
|
||||
virtual_orgs_ids = settings.VIRTUAL_UNIVERSITIES
|
||||
meta_orgs = getattr(settings, 'META_UNIVERSITIES', {})
|
||||
|
||||
# Get all the ids associated with this organization
|
||||
all_courses = modulestore().get_courses()
|
||||
valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES)
|
||||
if org_id not in valid_org_ids:
|
||||
valid_orgs_ids = set(c.org for c in all_courses)
|
||||
valid_orgs_ids.update(virtual_orgs_ids + meta_orgs.keys())
|
||||
|
||||
if org_id not in valid_orgs_ids:
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
courses = sort_by_announcement(courses)
|
||||
# Grab all courses for this organization(s)
|
||||
org_ids = set([org_id] + meta_orgs.get(org_id, []))
|
||||
org_courses = []
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
for key in org_ids:
|
||||
cs = get_courses_by_university(request.user, domain=domain)[key]
|
||||
org_courses.extend(cs)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
org_courses = sort_by_announcement(org_courses)
|
||||
|
||||
context = dict(courses=org_courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
return render_to_response(template_file, context)
|
||||
@@ -613,6 +634,7 @@ def progress(request, course_id, student_id=None):
|
||||
'courseware_summary': courseware_summary,
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
'student': student,
|
||||
}
|
||||
context.update()
|
||||
|
||||
@@ -646,13 +668,13 @@ def submission_history(request, course_id, student_username, location):
|
||||
.format(student_username, location))
|
||||
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
.filter(student_module=student_module).order_by('-id')
|
||||
|
||||
# If no history records exist, let's force a save to get history started.
|
||||
if not history_entries:
|
||||
student_module.save()
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
.filter(student_module=student_module).order_by('-id')
|
||||
|
||||
context = {
|
||||
'history_entries': history_entries,
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import json
|
||||
import logging
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.utils import simplejson
|
||||
from django.http import Http404
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import get_course_with_access
|
||||
from course_groups.cohorts import is_course_cohorted, get_cohort_id, is_commentable_cohorted, get_cohorted_commentables, get_cohort, get_course_cohorts, get_cohort_by_id
|
||||
from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted,
|
||||
get_cohorted_commentables, get_course_cohorts, get_cohort_by_id)
|
||||
from courseware.access import has_access
|
||||
|
||||
from urllib import urlencode
|
||||
from operator import methodcaller
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from django_comment_client.utils import (merge_dict, extract, strip_none,
|
||||
strip_blank, get_courseware_context)
|
||||
|
||||
from django_comment_client.permissions import cached_has_permission
|
||||
from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
|
||||
import django_comment_client.utils as utils
|
||||
import comment_client as cc
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
THREADS_PER_PAGE = 20
|
||||
INLINE_THREADS_PER_PAGE = 20
|
||||
@@ -31,6 +25,7 @@ escapedict = {'"': '"'}
|
||||
log = logging.getLogger("edx.discussions")
|
||||
|
||||
|
||||
@login_required
|
||||
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
|
||||
"""
|
||||
This may raise cc.utils.CommentClientError or
|
||||
@@ -60,7 +55,6 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
cc_user.default_sort_key = request.GET.get('sort_key')
|
||||
cc_user.save()
|
||||
|
||||
|
||||
#there are 2 dimensions to consider when executing a search with respect to group id
|
||||
#is user a moderator
|
||||
#did the user request a group
|
||||
@@ -91,18 +85,17 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
|
||||
#now add the group name if the thread has a group id
|
||||
for thread in threads:
|
||||
|
||||
|
||||
if thread.get('group_id'):
|
||||
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
|
||||
thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
|
||||
else:
|
||||
thread['group_name'] = ""
|
||||
thread['group_string'] = "This post visible to everyone."
|
||||
|
||||
|
||||
#patch for backward compatibility to comments service
|
||||
if not 'pinned' in thread:
|
||||
thread['pinned'] = False
|
||||
|
||||
|
||||
query_params['page'] = page
|
||||
query_params['num_pages'] = num_pages
|
||||
@@ -110,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
return threads, query_params
|
||||
|
||||
|
||||
@login_required
|
||||
def inline_discussion(request, course_id, discussion_id):
|
||||
"""
|
||||
Renders JSON for DiscussionModules
|
||||
@@ -142,14 +136,14 @@ def inline_discussion(request, course_id, discussion_id):
|
||||
cohorts_list = list()
|
||||
|
||||
if is_cohorted:
|
||||
cohorts_list.append({'name':'All Groups','id':None})
|
||||
cohorts_list.append({'name': 'All Groups', 'id': None})
|
||||
|
||||
#if you're a mod, send all cohorts and let you pick
|
||||
|
||||
if is_moderator:
|
||||
cohorts = get_course_cohorts(course_id)
|
||||
for c in cohorts:
|
||||
cohorts_list.append({'name':c.name, 'id':c.id})
|
||||
cohorts_list.append({'name': c.name, 'id': c.id})
|
||||
|
||||
else:
|
||||
#students don't get to choose
|
||||
@@ -216,9 +210,6 @@ def forum_form_discussion(request, course_id):
|
||||
|
||||
user_cohort_id = get_cohort_id(request.user, course_id)
|
||||
|
||||
|
||||
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
@@ -242,6 +233,7 @@ def forum_form_discussion(request, course_id):
|
||||
|
||||
return render_to_response('discussion/index.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def single_thread(request, course_id, discussion_id, thread_id):
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
@@ -250,11 +242,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
|
||||
try:
|
||||
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
|
||||
|
||||
|
||||
#patch for backward compatibility with comments service
|
||||
if not 'pinned' in thread.attributes:
|
||||
thread['pinned'] = False
|
||||
|
||||
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
|
||||
log.error("Error loading single thread.")
|
||||
raise Http404
|
||||
@@ -352,7 +344,7 @@ def user_profile(request, course_id, user_id):
|
||||
query_params = {
|
||||
'page': request.GET.get('page', 1),
|
||||
'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities
|
||||
}
|
||||
}
|
||||
|
||||
threads, page, num_pages = profiled_user.active_threads(query_params)
|
||||
query_params['page'] = page
|
||||
@@ -369,8 +361,6 @@ def user_profile(request, course_id, user_id):
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
})
|
||||
else:
|
||||
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'user': request.user,
|
||||
@@ -426,5 +416,5 @@ def followed_threads(request, course_id, user_id):
|
||||
}
|
||||
|
||||
return render_to_response('discussion/user_profile.html', context)
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
|
||||
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
|
||||
raise Http404
|
||||
|
||||
@@ -6,7 +6,8 @@ Enrollments.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import CourseEnrollment, assign_default_role
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.models import assign_default_role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -6,7 +6,8 @@ Enrollments.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from student.models import CourseEnrollment, assign_default_role
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.models import assign_default_role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Reload forum (comment client) users from existing users.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
import comment_client as cc
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Reload forum (comment client) users from existing users'
|
||||
|
||||
def adduser(self,user):
|
||||
print user
|
||||
try:
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
cc_user.save()
|
||||
except Exception as err:
|
||||
print "update user info to discussion failed for user with id: %s" % user
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 0:
|
||||
uset = [User.objects.get(username=x) for x in args]
|
||||
else:
|
||||
uset = User.objects.all()
|
||||
|
||||
for user in uset:
|
||||
self.adduser(user)
|
||||
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
from comment_client import CommentClientError
|
||||
from django_comment_client.utils import JsonError
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AjaxExceptionMiddleware(object):
|
||||
"""
|
||||
Middleware that captures CommentClientErrors during ajax requests
|
||||
and tranforms them into json responses
|
||||
"""
|
||||
def process_exception(self, request, exception):
|
||||
"""
|
||||
Processes CommentClientErrors in ajax requests. If the request is an ajax request,
|
||||
returns a http response that encodes the error as json
|
||||
"""
|
||||
if isinstance(exception, CommentClientError) and request.is_ajax():
|
||||
return JsonError(json.loads(exception.message))
|
||||
try:
|
||||
return JsonError(json.loads(exception.message))
|
||||
except ValueError:
|
||||
return JsonError(exception.message)
|
||||
return None
|
||||
|
||||
@@ -1,73 +1,12 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.conf import settings
|
||||
|
||||
from mock import Mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save
|
||||
from django.dispatch.dispatcher import _make_id
|
||||
import string
|
||||
import random
|
||||
from .permissions import has_permission
|
||||
from .models import Role, Permission
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
import comment_client
|
||||
|
||||
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
|
||||
|
||||
#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
#class TestCohorting(PageLoader):
|
||||
# """Check that cohorting works properly"""
|
||||
#
|
||||
# def setUp(self):
|
||||
# xmodule.modulestore.django._MODULESTORES = {}
|
||||
#
|
||||
# # Assume courses are there
|
||||
# self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
#
|
||||
# # Create two accounts
|
||||
# self.student = 'view@test.com'
|
||||
# self.student2 = 'view2@test.com'
|
||||
# self.password = 'foo'
|
||||
# self.create_account('u1', self.student, self.password)
|
||||
# self.create_account('u2', self.student2, self.password)
|
||||
# self.activate_user(self.student)
|
||||
# self.activate_user(self.student2)
|
||||
#
|
||||
# def test_create_thread(self):
|
||||
# my_save = Mock()
|
||||
# comment_client.perform_request = my_save
|
||||
#
|
||||
# resp = self.client.post(
|
||||
# reverse('django_comment_client.base.views.create_thread',
|
||||
# kwargs={'course_id': 'edX/toy/2012_Fall',
|
||||
# 'commentable_id': 'General'}),
|
||||
# {'some': "some",
|
||||
# 'data': 'data'})
|
||||
# self.assertTrue(my_save.called)
|
||||
#
|
||||
# #self.assertEqual(resp.status_code, 200)
|
||||
# #self.assertEqual(my_save.something, "expected", "complaint if not true")
|
||||
#
|
||||
# self.toy.cohort_config = {"cohorted": True}
|
||||
#
|
||||
# # call the view again ...
|
||||
#
|
||||
# # assert that different things happened
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
from django_comment_client.permissions import has_permission
|
||||
from django_comment_client.models import Role
|
||||
|
||||
|
||||
class PermissionsTestCase(TestCase):
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import string
|
||||
import random
|
||||
import collections
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
import comment_client
|
||||
@@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase):
|
||||
|
||||
# TODO: check whether the correct error message is produced.
|
||||
# The error message should be the same as the argument to CommentClientError
|
||||
def setUp(self):
|
||||
self.a = middleware.AjaxExceptionMiddleware()
|
||||
self.request1 = django.http.HttpRequest()
|
||||
self.request0 = django.http.HttpRequest()
|
||||
self.exception1 = comment_client.CommentClientError('{}')
|
||||
self.exception0 = ValueError()
|
||||
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
|
||||
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
|
||||
def setUp(self):
|
||||
self.a = middleware.AjaxExceptionMiddleware()
|
||||
self.request1 = django.http.HttpRequest()
|
||||
self.request0 = django.http.HttpRequest()
|
||||
self.exception1 = comment_client.CommentClientError('{}')
|
||||
self.exception2 = comment_client.CommentClientError('Foo!')
|
||||
self.exception0 = ValueError()
|
||||
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
|
||||
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
|
||||
|
||||
def test_process_exception(self):
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError)
|
||||
self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception1))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception0))
|
||||
def test_process_exception(self):
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError)
|
||||
self.assertIsInstance(self.a.process_exception(self.request1, self.exception2), middleware.JsonError)
|
||||
self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception1))
|
||||
self.assertIsNone(self.a.process_exception(self.request0, self.exception0))
|
||||
|
||||
@@ -8,13 +8,6 @@ Notes for running by hand:
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
|
||||
"""
|
||||
|
||||
import courseware.tests.tests as ct
|
||||
|
||||
import json
|
||||
|
||||
from nose import SkipTest
|
||||
from mock import patch, Mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
@@ -26,13 +19,13 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
from django_comment_client.utils import has_forum_access
|
||||
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for download of csv
|
||||
'''
|
||||
@@ -55,7 +48,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
g.user_set.add(get_user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
@@ -63,7 +56,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.toy)
|
||||
|
||||
|
||||
def test_download_grades_csv(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
@@ -82,8 +74,8 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
|
||||
# All the not-actually-in-the-course hw and labs come from the
|
||||
# default grading policy string in graders.py
|
||||
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm 01","Midterm Avg","Final 01","Final Avg"
|
||||
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
|
||||
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
|
||||
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
|
||||
'''
|
||||
|
||||
self.assertEqual(body, expected_body, msg)
|
||||
@@ -101,9 +93,8 @@ def action_name(operation, rolename):
|
||||
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
||||
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check for change in forum admin role memberships
|
||||
'''
|
||||
@@ -112,7 +103,6 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
|
||||
@@ -127,14 +117,12 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
g.user_set.add(get_user(self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.toy)
|
||||
|
||||
|
||||
|
||||
def initialize_roles(self, course_id):
|
||||
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
|
||||
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
|
||||
|
||||
@@ -41,6 +41,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
import xmodule.graders as xmgraders
|
||||
import track.views
|
||||
|
||||
from .offline_gradecalc import student_grades, offline_grades_available
|
||||
@@ -92,13 +93,13 @@ def instructor_dashboard(request, course_id):
|
||||
data += compute_course_stats(course).items()
|
||||
if request.user.is_staff:
|
||||
for field in course.fields:
|
||||
if getattr(field.scope, 'student', False):
|
||||
if getattr(field.scope, 'user', False):
|
||||
continue
|
||||
|
||||
data.append([field.name, json.dumps(field.read_json(course))])
|
||||
for namespace in course.namespaces:
|
||||
for field in getattr(course, namespace).fields:
|
||||
if getattr(field.scope, 'student', False):
|
||||
if getattr(field.scope, 'user', False):
|
||||
continue
|
||||
|
||||
data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))])
|
||||
@@ -208,6 +209,10 @@ def instructor_dashboard(request, course_id):
|
||||
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
|
||||
return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
|
||||
|
||||
elif 'Dump description of graded assignments configuration' in action:
|
||||
track.views.server_track(request, action, {}, page='idashboard')
|
||||
msg += dump_grading_context(course)
|
||||
|
||||
elif "Reset student's attempts" in action or "Delete student state for problem" in action:
|
||||
# get the form data
|
||||
unique_student_identifier = request.POST.get('unique_student_identifier', '')
|
||||
@@ -229,9 +234,11 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
if student_to_reset is not None:
|
||||
# find the module in question
|
||||
if '/' not in problem_to_reset: # allow state of modules other than problem to be reset
|
||||
problem_to_reset = "problem/" + problem_to_reset # but problem is the default
|
||||
try:
|
||||
(org, course_name, run) = course_id.split("/")
|
||||
module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_reset
|
||||
module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
|
||||
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
|
||||
course_id=course_id,
|
||||
module_state_key=module_state_key)
|
||||
@@ -1120,3 +1127,50 @@ def compute_course_stats(course):
|
||||
walk(course)
|
||||
stats = dict(counts) # number of each kind of module
|
||||
return stats
|
||||
|
||||
|
||||
def dump_grading_context(course):
|
||||
'''
|
||||
Dump information about course grading context (eg which problems are graded in what assignments)
|
||||
Very useful for debugging grading_policy.json and policy.json
|
||||
'''
|
||||
msg = "-----------------------------------------------------------------------------\n"
|
||||
msg += "Course grader:\n"
|
||||
|
||||
msg += '%s\n' % course.grader.__class__
|
||||
graders = {}
|
||||
if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader):
|
||||
msg += '\n'
|
||||
msg += "Graded sections:\n"
|
||||
for subgrader, category, weight in course.grader.sections:
|
||||
msg += " subgrader=%s, type=%s, category=%s, weight=%s\n" % (subgrader.__class__, subgrader.type, category, weight)
|
||||
subgrader.index = 1
|
||||
graders[subgrader.type] = subgrader
|
||||
msg += "-----------------------------------------------------------------------------\n"
|
||||
msg += "Listing grading context for course %s\n" % course.id
|
||||
|
||||
gc = course.grading_context
|
||||
msg += "graded sections:\n"
|
||||
|
||||
msg += '%s\n' % gc['graded_sections'].keys()
|
||||
for (gs, gsvals) in gc['graded_sections'].items():
|
||||
msg += "--> Section %s:\n" % (gs)
|
||||
for sec in gsvals:
|
||||
s = sec['section_descriptor']
|
||||
format = getattr(s.lms, 'format', None)
|
||||
aname = ''
|
||||
if format in graders:
|
||||
g = graders[format]
|
||||
aname = '%s %02d' % (g.short_label, g.index)
|
||||
g.index += 1
|
||||
elif s.display_name in graders:
|
||||
g = graders[s.display_name]
|
||||
aname = '%s' % g.short_label
|
||||
notes = ''
|
||||
if getattr(s, 'score_by_attempt', False):
|
||||
notes = ', score by attempt!'
|
||||
msg += " %s (format=%s, Assignment=%s%s)\n" % (s.display_name, format, aname, notes)
|
||||
msg += "all descriptors:\n"
|
||||
msg += "length=%d\n" % len(gc['all_descriptors'])
|
||||
msg = '<pre>%s</pre>' % msg.replace('<','<')
|
||||
return msg
|
||||
|
||||
@@ -1,22 +1,138 @@
|
||||
"""Tests for License package"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from uuid import uuid4
|
||||
from random import shuffle
|
||||
from tempfile import NamedTemporaryFile
|
||||
from factory import Factory, SubFactory
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.management import call_command
|
||||
|
||||
from .models import CourseSoftware, UserLicense
|
||||
from django.core.urlresolvers import reverse
|
||||
from licenses.models import CourseSoftware, UserLicense
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, get_user
|
||||
|
||||
COURSE_1 = 'edX/toy/2012_Fall'
|
||||
|
||||
SOFTWARE_1 = 'matlab'
|
||||
SOFTWARE_2 = 'stata'
|
||||
|
||||
SERIAL_1 = '123456abcde'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseSoftwareFactory(Factory):
|
||||
'''Factory for generating CourseSoftware objects in database'''
|
||||
FACTORY_FOR = CourseSoftware
|
||||
|
||||
name = SOFTWARE_1
|
||||
full_name = SOFTWARE_1
|
||||
url = SOFTWARE_1
|
||||
course_id = COURSE_1
|
||||
|
||||
|
||||
class UserLicenseFactory(Factory):
|
||||
'''
|
||||
Factory for generating UserLicense objects in database
|
||||
|
||||
By default, the user assigned is null, indicating that the
|
||||
serial number has not yet been assigned.
|
||||
'''
|
||||
FACTORY_FOR = UserLicense
|
||||
|
||||
software = SubFactory(CourseSoftwareFactory)
|
||||
serial = SERIAL_1
|
||||
|
||||
|
||||
class LicenseTestCase(LoginEnrollmentTestCase):
|
||||
'''Tests for licenses.views'''
|
||||
def setUp(self):
|
||||
'''creates a user and logs in'''
|
||||
self.setup_viewtest_user()
|
||||
self.software = CourseSoftwareFactory()
|
||||
|
||||
def test_get_license(self):
|
||||
UserLicenseFactory(user=get_user(self.viewtest_email), software=self.software)
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(200, response.status_code)
|
||||
json_returned = json.loads(response.content)
|
||||
self.assertFalse('error' in json_returned)
|
||||
self.assertTrue('serial' in json_returned)
|
||||
self.assertEquals(json_returned['serial'], SERIAL_1)
|
||||
|
||||
def test_get_nonexistent_license(self):
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(200, response.status_code)
|
||||
json_returned = json.loads(response.content)
|
||||
self.assertFalse('serial' in json_returned)
|
||||
self.assertTrue('error' in json_returned)
|
||||
|
||||
def test_create_nonexistent_license(self):
|
||||
'''Should not assign a license to an unlicensed user when none are available'''
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'true'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(200, response.status_code)
|
||||
json_returned = json.loads(response.content)
|
||||
self.assertFalse('serial' in json_returned)
|
||||
self.assertTrue('error' in json_returned)
|
||||
|
||||
def test_create_license(self):
|
||||
'''Should assign a license to an unlicensed user if one is unassigned'''
|
||||
# create an unassigned license
|
||||
UserLicenseFactory(software=self.software)
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'true'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(200, response.status_code)
|
||||
json_returned = json.loads(response.content)
|
||||
self.assertFalse('error' in json_returned)
|
||||
self.assertTrue('serial' in json_returned)
|
||||
self.assertEquals(json_returned['serial'], SERIAL_1)
|
||||
|
||||
def test_get_license_from_wrong_course(self):
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format('some/other/course'))
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_get_license_from_non_ajax(self):
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_get_license_without_software(self):
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_get_license_without_login(self):
|
||||
self.logout()
|
||||
response = self.client.post(reverse('user_software_license'),
|
||||
{'software': SOFTWARE_1, 'generate': 'false'},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
HTTP_REFERER='/courses/{0}/some_page'.format(COURSE_1))
|
||||
# if we're not logged in, we should be referred to the login page
|
||||
self.assertEqual(302, response.status_code)
|
||||
|
||||
|
||||
class CommandTest(TestCase):
|
||||
'''Test management command for importing serial numbers'''
|
||||
|
||||
def test_import_serial_numbers(self):
|
||||
size = 20
|
||||
|
||||
@@ -51,31 +167,33 @@ class CommandTest(TestCase):
|
||||
licenses_count = UserLicense.objects.all().count()
|
||||
self.assertEqual(3 * size, licenses_count)
|
||||
|
||||
cs = CourseSoftware.objects.get(pk=1)
|
||||
software = CourseSoftware.objects.get(pk=1)
|
||||
|
||||
lics = UserLicense.objects.filter(software=cs)[:size]
|
||||
lics = UserLicense.objects.filter(software=software)[:size]
|
||||
known_serials = list(l.serial for l in lics)
|
||||
known_serials.extend(generate_serials(10))
|
||||
|
||||
shuffle(known_serials)
|
||||
|
||||
log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
|
||||
with NamedTemporaryFile() as f:
|
||||
f.write('\n'.join(known_serials))
|
||||
f.flush()
|
||||
args = [COURSE_1, SOFTWARE_1, f.name]
|
||||
with NamedTemporaryFile() as tmpfile:
|
||||
tmpfile.write('\n'.join(known_serials))
|
||||
tmpfile.flush()
|
||||
args = [COURSE_1, SOFTWARE_1, tmpfile.name]
|
||||
call_command('import_serial_numbers', *args)
|
||||
|
||||
log.debug('Check if we added only the new ones')
|
||||
licenses_count = UserLicense.objects.filter(software=cs).count()
|
||||
licenses_count = UserLicense.objects.filter(software=software).count()
|
||||
self.assertEqual((2 * size) + 10, licenses_count)
|
||||
|
||||
|
||||
def generate_serials(size=20):
|
||||
'''generate a list of serial numbers'''
|
||||
return [str(uuid4()) for _ in range(size)]
|
||||
|
||||
|
||||
def generate_serials_file(size=20):
|
||||
'''output list of generated serial numbers to a temp file'''
|
||||
serials = generate_serials(size)
|
||||
|
||||
temp_file = NamedTemporaryFile()
|
||||
|
||||
@@ -7,12 +7,13 @@ from collections import namedtuple, defaultdict
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.views.decorators.csrf import requires_csrf_token, csrf_protect
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
|
||||
from .models import CourseSoftware
|
||||
from .models import get_courses_licenses, get_or_create_license, get_license
|
||||
from licenses.models import CourseSoftware
|
||||
from licenses.models import get_courses_licenses, get_or_create_license, get_license
|
||||
|
||||
|
||||
log = logging.getLogger("mitx.licenses")
|
||||
@@ -44,6 +45,7 @@ def get_licenses_by_course(user, courses):
|
||||
return data_by_course
|
||||
|
||||
|
||||
@login_required
|
||||
@requires_csrf_token
|
||||
def user_software_license(request):
|
||||
if request.method != 'POST' or not request.is_ajax():
|
||||
@@ -65,19 +67,21 @@ def user_software_license(request):
|
||||
try:
|
||||
software = CourseSoftware.objects.get(name=software_name,
|
||||
course_id=course_id)
|
||||
print software
|
||||
except CourseSoftware.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
user = User.objects.get(id=user_id)
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
if generate:
|
||||
license = get_or_create_license(user, software)
|
||||
software_license = get_or_create_license(user, software)
|
||||
else:
|
||||
license = get_license(user, software)
|
||||
software_license = get_license(user, software)
|
||||
|
||||
if license:
|
||||
response = {'serial': license.serial}
|
||||
if software_license:
|
||||
response = {'serial': software_license.serial}
|
||||
else:
|
||||
response = {'error': 'No serial number found'}
|
||||
|
||||
|
||||
@@ -39,12 +39,14 @@ def getip(request):
|
||||
|
||||
|
||||
def get_commit_id(course):
|
||||
return course.metadata.get('GIT_COMMIT_ID', 'No commit id')
|
||||
#return course.metadata.get('GIT_COMMIT_ID', 'No commit id')
|
||||
return getattr(course, 'GIT_COMMIT_ID', 'No commit id')
|
||||
# getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id')
|
||||
|
||||
|
||||
def set_commit_id(course, commit_id):
|
||||
course.metadata['GIT_COMMIT_ID'] = commit_id
|
||||
#course.metadata['GIT_COMMIT_ID'] = commit_id
|
||||
setattr(course, 'GIT_COMMIT_ID', commit_id)
|
||||
# setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id)
|
||||
|
||||
|
||||
@@ -124,7 +126,8 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
|
||||
|
||||
#----------------------------------------
|
||||
|
||||
dumpfields = ['definition', 'location', 'metadata']
|
||||
#dumpfields = ['definition', 'location', 'metadata']
|
||||
dumpfields = ['location', 'metadata']
|
||||
|
||||
for cdir, course in def_ms.courses.items():
|
||||
html += '<hr width="100%"/>'
|
||||
@@ -133,7 +136,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
|
||||
html += '<p>commit_id=%s</p>' % get_commit_id(course)
|
||||
|
||||
for field in dumpfields:
|
||||
data = getattr(course, field)
|
||||
data = getattr(course, field, None)
|
||||
html += '<h3>%s</h3>' % field
|
||||
if type(data) == dict:
|
||||
html += '<ul>'
|
||||
|
||||
@@ -4,22 +4,22 @@ Tests for open ended grading interfaces
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from open_ended_grading import staff_grading_service
|
||||
from xmodule.open_ended_grading_classes import peer_grading_service
|
||||
from xmodule import peer_grading_module
|
||||
import json
|
||||
from mock import MagicMock
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import Group
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from xmodule.open_ended_grading_classes import peer_grading_service
|
||||
from xmodule import peer_grading_module
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
from nose import SkipTest
|
||||
from mock import patch, Mock, MagicMock
|
||||
import json
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from open_ended_grading import staff_grading_service
|
||||
from courseware.access import _course_staff_group_name
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
|
||||
|
||||
import logging
|
||||
|
||||
@@ -30,8 +30,8 @@ from django.http import QueryDict
|
||||
from xmodule.tests import test_util_open_ended
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffGradingService(ct.PageLoader):
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffGradingService(LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check that staff grading service proxy works. Basically just checking the
|
||||
access control and error handling logic -- all the actual work is on the
|
||||
@@ -56,7 +56,7 @@ class TestStaffGradingService(ct.PageLoader):
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
g.user_set.add(get_user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
@@ -126,8 +126,8 @@ class TestStaffGradingService(ct.PageLoader):
|
||||
self.assertIsNotNone(d['problem_list'])
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestPeerGradingService(ct.PageLoader):
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestPeerGradingService(LoginEnrollmentTestCase):
|
||||
'''
|
||||
Check that staff grading service proxy works. Basically just checking the
|
||||
access control and error handling logic -- all the actual work is on the
|
||||
|
||||
@@ -111,7 +111,7 @@ def peer_grading(request, course_id):
|
||||
#Get the peer grading modules currently in the course
|
||||
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
|
||||
#See if any of the modules are centralized modules (ie display info from multiple problems)
|
||||
items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict]
|
||||
items = [i for i in items if getattr(i,"use_for_single_location", True) in false_dict]
|
||||
#Get the first one
|
||||
item_location = items[0].location
|
||||
#Generate a url for the first module and redirect the user to it
|
||||
|
||||
@@ -15,7 +15,6 @@ from scipy.optimize import curve_fit
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum, Max
|
||||
from psychometrics.models import *
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
log = logging.getLogger("mitx.psychometrics")
|
||||
|
||||
@@ -246,13 +245,16 @@ def generate_plots_for_problem(problem):
|
||||
yset['ydat'] = ydat
|
||||
|
||||
if len(ydat) > 3: # try to fit to logistic function if enough data points
|
||||
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
|
||||
yset['fitparam'] = cfp
|
||||
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
|
||||
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
|
||||
fitx = np.linspace(xdat[0], xdat[-1], 100)
|
||||
yset['fitx'] = fitx
|
||||
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
|
||||
try:
|
||||
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
|
||||
yset['fitparam'] = cfp
|
||||
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
|
||||
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
|
||||
fitx = np.linspace(xdat[0], xdat[-1], 100)
|
||||
yset['fitx'] = fitx
|
||||
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
|
||||
except Exception as err:
|
||||
log.debug('Error in psychoanalyze curve fitting: %s' % err)
|
||||
|
||||
dataset['grade_%d' % grade] = yset
|
||||
|
||||
@@ -289,7 +291,7 @@ def generate_plots_for_problem(problem):
|
||||
'info': '',
|
||||
'data': jsdata,
|
||||
'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
|
||||
})
|
||||
})
|
||||
|
||||
#log.debug('plots = %s' % plots)
|
||||
return msg, plots
|
||||
@@ -302,12 +304,12 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
|
||||
Construct and return a procedure which may be called to update
|
||||
the PsychometricsData instance for the given StudentModule instance.
|
||||
"""
|
||||
sm = studentmodule.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key=module_state_key,
|
||||
defaults={'state': '{}', 'module_type': 'problem'},
|
||||
)
|
||||
sm, status = StudentModule.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key=module_state_key,
|
||||
defaults={'state': '{}', 'module_type': 'problem'},
|
||||
)
|
||||
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
@@ -329,7 +331,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
|
||||
return
|
||||
|
||||
pmd.done = done
|
||||
pmd.attempts = state['attempts']
|
||||
try:
|
||||
pmd.attempts = state.get('attempts', 0)
|
||||
except:
|
||||
log.exception("no attempts for %s (state=%s)" % (sm, sm.state))
|
||||
|
||||
try:
|
||||
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
|
||||
except:
|
||||
|
||||
@@ -29,6 +29,14 @@ MODULESTORE = {
|
||||
}
|
||||
}
|
||||
|
||||
CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'OPTIONS': {
|
||||
'host': 'localhost',
|
||||
'db': 'test_xcontent',
|
||||
}
|
||||
}
|
||||
|
||||
# Set this up so that rake lms[acceptance] and running the
|
||||
# harvest command both use the same (test) database
|
||||
# which they can flush without messing up your dev db
|
||||
@@ -40,6 +48,18 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
# Set up XQueue information so that the lms will send
|
||||
# requests to a mock XQueue server running locally
|
||||
XQUEUE_PORT = 8027
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://127.0.0.1:%d" % XQUEUE_PORT,
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
},
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
# Do not display the YouTube videos in the browser while running the
|
||||
# acceptance tests. This makes them faster and more reliable
|
||||
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
|
||||
|
||||
@@ -76,6 +76,7 @@ LOGGING = get_logger_config(LOG_DIR,
|
||||
COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
|
||||
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
|
||||
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
|
||||
META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
|
||||
@@ -9,6 +9,7 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
|
||||
SUBDOMAIN_BRANDING['edge'] = 'edge'
|
||||
SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
|
||||
VIRTUAL_UNIVERSITIES = ['edge']
|
||||
META_UNIVERSITIES = {}
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
|
||||
@@ -364,6 +364,7 @@ TEMPLATE_LOADERS = (
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'contentserver.middleware.StaticContentServer',
|
||||
'request_cache.middleware.RequestCache',
|
||||
'django_comment_client.middleware.AjaxExceptionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
||||
@@ -113,6 +113,9 @@ SUBDOMAIN_BRANDING = {
|
||||
# have an actual course with that org set
|
||||
VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
# Organization that contain other organizations
|
||||
META_UNIVERSITIES = {'UTx': ['UTAustinX']}
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
############################## Course static files ##########################
|
||||
@@ -218,7 +221,7 @@ FILE_UPLOAD_HANDLERS = (
|
||||
|
||||
########################### PIPELINE #################################
|
||||
|
||||
PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
|
||||
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
|
||||
|
||||
########################## PEARSON TESTING ###########################
|
||||
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
|
||||
|
||||
@@ -74,6 +74,15 @@ def to_latex(x):
|
||||
# LatexPrinter._print_dot = _print_dot
|
||||
xs = latex(x)
|
||||
xs = xs.replace(r'\XI', 'XI') # workaround for strange greek
|
||||
|
||||
# substitute back into latex form for scripts
|
||||
# literally something of the form
|
||||
# 'scriptN' becomes '\\mathcal{N}'
|
||||
# note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms
|
||||
xs = re.sub(r'script([a-zA-Z0-9]+)',
|
||||
'\\mathcal{\\1}',
|
||||
xs)
|
||||
|
||||
#return '<math>%s{}{}</math>' % (xs[1:-1])
|
||||
if xs[0] == '$':
|
||||
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6
|
||||
@@ -106,6 +115,7 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
|
||||
'i': sympy.I, # lowercase i is also sqrt(-1)
|
||||
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
|
||||
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
|
||||
'N': sympy.Symbol('N'), # or it is some kind of sympy function
|
||||
#'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
|
||||
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
|
||||
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
|
||||
@@ -247,6 +257,127 @@ class formula(object):
|
||||
fix_hat(k)
|
||||
fix_hat(xml)
|
||||
|
||||
def flatten_pmathml(xml):
|
||||
''' Give the text version of certain PMathML elements
|
||||
|
||||
Sometimes MathML will be given with each letter separated (it
|
||||
doesn't know if its implicit multiplication or what). From an xml
|
||||
node, find the (text only) variable name it represents. So it takes
|
||||
<mrow>
|
||||
<mi>m</mi>
|
||||
<mi>a</mi>
|
||||
<mi>x</mi>
|
||||
</mrow>
|
||||
and returns 'max', for easier use later on.
|
||||
'''
|
||||
tag = gettag(xml)
|
||||
if tag == 'mn': return xml.text
|
||||
elif tag == 'mi': return xml.text
|
||||
elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml])
|
||||
raise Exception, '[flatten_pmathml] unknown tag %s' % tag
|
||||
|
||||
def fix_mathvariant(parent):
|
||||
'''Fix certain kinds of math variants
|
||||
|
||||
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
|
||||
with 'scriptN'. There have been problems using script_N or script(N)
|
||||
'''
|
||||
for child in parent:
|
||||
if (gettag(child) == 'mstyle' and child.get('mathvariant') == 'script'):
|
||||
newchild = etree.Element('mi')
|
||||
newchild.text = 'script%s' % flatten_pmathml(child[0])
|
||||
parent.replace(child, newchild)
|
||||
fix_mathvariant(child)
|
||||
fix_mathvariant(xml)
|
||||
|
||||
|
||||
# find "tagged" superscripts
|
||||
# they have the character \u200b in the superscript
|
||||
# replace them with a__b so snuggle doesn't get confused
|
||||
def fix_superscripts(xml):
|
||||
''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
|
||||
|
||||
In the javascript, variables with '__X' in them had an invisible
|
||||
character inserted into the sup (to distinguish from powers)
|
||||
E.g. normal:
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>
|
||||
to be interpreted '(a_b)^c' (nothing done by this method)
|
||||
|
||||
And modified:
|
||||
<msubsup>
|
||||
<mi>b</mi>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>d</mi>
|
||||
</mrow>
|
||||
</msubsup>
|
||||
to be interpreted 'a_b__c'
|
||||
|
||||
also:
|
||||
<msup>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>B</mi>
|
||||
</mrow>
|
||||
</msup>
|
||||
to be 'x__B'
|
||||
'''
|
||||
for k in xml:
|
||||
tag = gettag(k)
|
||||
|
||||
# match things like the last example--
|
||||
# the second item in msub is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (tag == 'msup' and
|
||||
len(k) == 2 and gettag(k[1]) == 'mrow' and
|
||||
gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew
|
||||
|
||||
# replace the msup with 'X__Y'
|
||||
k[1].remove(k[1][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
# match things like the middle example-
|
||||
# the third item in msubsup is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (tag == 'msubsup' and
|
||||
len(k) == 3 and gettag(k[2]) == 'mrow' and
|
||||
gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew
|
||||
|
||||
# replace the msubsup with 'X_Y__Z'
|
||||
k[2].remove(k[2][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
fix_superscripts(k)
|
||||
fix_superscripts(xml)
|
||||
|
||||
# Snuggle returns an error when it sees an <msubsup>
|
||||
# replace such elements with an <msup>, except the first element is of
|
||||
# the form a_b. I.e. map a_b^c => (a_b)^c
|
||||
def fix_msubsup(parent):
|
||||
for child in parent:
|
||||
# fix msubsup
|
||||
if (gettag(child) == 'msubsup' and len(child) == 3):
|
||||
newchild = etree.Element('msup')
|
||||
newbase = etree.Element('mi')
|
||||
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
|
||||
newexp = child[2]
|
||||
newchild.append(newbase)
|
||||
newchild.append(newexp)
|
||||
parent.replace(child, newchild)
|
||||
|
||||
fix_msubsup(child)
|
||||
fix_msubsup(xml)
|
||||
|
||||
self.xml = xml
|
||||
return self.xml
|
||||
|
||||
@@ -257,6 +388,7 @@ class formula(object):
|
||||
try:
|
||||
xml = self.preprocess_pmathml(self.expr)
|
||||
except Exception, err:
|
||||
log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr))
|
||||
return "<html>Error! Cannot process pmathml</html>"
|
||||
pmathml = etree.tostring(xml, pretty_print=True)
|
||||
self.the_pmathml = pmathml
|
||||
|
||||
115
lms/lib/symmath/test_formula.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Tests of symbolic math
|
||||
"""
|
||||
|
||||
|
||||
import unittest
|
||||
import formula
|
||||
import re
|
||||
from lxml import etree
|
||||
|
||||
def stripXML(xml):
|
||||
xml = xml.replace('\n', '')
|
||||
xml = re.sub(r'\> +\<', '><', xml)
|
||||
return xml
|
||||
|
||||
class FormulaTest(unittest.TestCase):
|
||||
# for readability later
|
||||
mathml_start = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mstyle displaystyle="true">'
|
||||
mathml_end = '</mstyle></math>'
|
||||
|
||||
def setUp(self):
|
||||
self.formulaInstance = formula.formula('')
|
||||
|
||||
def test_replace_mathvariants(self):
|
||||
expr = '''
|
||||
<mstyle mathvariant="script">
|
||||
<mi>N</mi>
|
||||
</mstyle>'''
|
||||
|
||||
expected = '<mi>scriptN</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
|
||||
def test_fix_simple_superscripts(self):
|
||||
expr = '''
|
||||
<msup>
|
||||
<mi>a</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>b</mi>
|
||||
</mrow>
|
||||
</msup>'''
|
||||
|
||||
expected = '<mi>a__b</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
def test_fix_complex_superscripts(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>c</mi>
|
||||
</mrow>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<mi>a_b__c</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
|
||||
|
||||
def test_fix_msubsup(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<msup><mi>a_b</mi><mi>c</mi></msup>' # which is (a_b)^c
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
self.assertEqual(test, expected)
|
||||
@@ -2,13 +2,15 @@ import logging
|
||||
from dogapi import dog_http_api, dog_stats_api
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from request_cache.middleware import RequestCache
|
||||
|
||||
from django.core.cache import get_cache, InvalidCacheBackendError
|
||||
|
||||
cache = get_cache('mongo_metadata_inheritance')
|
||||
for store_name in settings.MODULESTORE:
|
||||
store = modulestore(store_name)
|
||||
store.metadata_inheritance_cache = cache
|
||||
store.metadata_inheritance_cache_subsystem = cache
|
||||
store.request_cache = RequestCache.get_request_cache()
|
||||
|
||||
if hasattr(settings, 'DATADOG_API'):
|
||||
dog_http_api.api_key = settings.DATADOG_API
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
lms/static/images/press/releases/stanford-university_102x57.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
lms/static/images/press/releases/stanford-university_204x114.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
lms/static/images/press/releases/stanford-university_240x135.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -1,5 +1,9 @@
|
||||
div.book-wrapper {
|
||||
@extend .table-wrapper;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
padding: 1em 8em;
|
||||
|
||||
|
||||
#open_close_accordion {
|
||||
display: none;
|
||||
@@ -11,7 +15,9 @@ div.book-wrapper {
|
||||
@include box-sizing(border-box);
|
||||
padding: 10px 0;
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-right: 1px solid #ccc;
|
||||
border: 1px solid #ccc;
|
||||
border-right: none;
|
||||
width: 250px;
|
||||
|
||||
ul#booknav {
|
||||
font-size: em(14);
|
||||
@@ -86,6 +92,8 @@ div.book-wrapper {
|
||||
|
||||
section.book {
|
||||
@extend .content;
|
||||
padding: 0;
|
||||
width:76%;
|
||||
|
||||
nav {
|
||||
@extend .clearfix;
|
||||
@@ -152,8 +160,12 @@ div.book-wrapper {
|
||||
|
||||
section.page {
|
||||
border: 1px solid $border-color;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: lh();
|
||||
margin-right:10%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
@@ -161,11 +173,8 @@ div.book-wrapper {
|
||||
|
||||
div {
|
||||
text-align: left;
|
||||
line-height: 1.6em;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.6em;
|
||||
margin: 5px;
|
||||
|
||||
.Paragraph, h2 {
|
||||
margin-top: 10px;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
left: 0;
|
||||
margin: 100px auto;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
height:500px;
|
||||
}
|
||||
|
||||
.login-box input[type=submit] {
|
||||
@@ -18,75 +18,18 @@
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#lean_overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 100;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
</%block>
|
||||
|
||||
<section id="login-modal" class="modal login-modal login-box">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Log In</h2>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email">
|
||||
<label>Password</label>
|
||||
<input name="password" type="password">
|
||||
<label class="remember-me">
|
||||
<input name="remember" type="checkbox" value="true">
|
||||
Remember me
|
||||
</label>
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Access My Courses">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="login-extra">
|
||||
<p>
|
||||
<span>Not enrolled? <a href="#signup-modal" class="close-login" rel="leanModal">Sign up.</a></span>
|
||||
<a href="#forgot-password-modal" rel="leanModal" class="pwd-reset">Forgot password?</a>
|
||||
</p>
|
||||
% if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
|
||||
<p>
|
||||
<a href="${MITX_ROOT_URL}/openid/login/">login via openid</a>
|
||||
</p>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class='login-box'></section>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
next = getParameterByName('next');
|
||||
if(next) {
|
||||
location.href = next;
|
||||
} else {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
} else {
|
||||
if($('#login_error').length == 0) {
|
||||
$('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>');
|
||||
}
|
||||
$('#login_error').html(json.value).stop().css("display", "block");
|
||||
$(document).ready(
|
||||
function() {
|
||||
// show dialog
|
||||
$('#login').click();
|
||||
}
|
||||
});
|
||||
);
|
||||
})(this)
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from xmodule.util.date_utils import get_default_time_display %>
|
||||
|
||||
<%def name="make_chapter(chapter)">
|
||||
<div class="chapter">
|
||||
@@ -10,7 +11,7 @@
|
||||
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
|
||||
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
|
||||
<p>${section['display_name']}</p>
|
||||
<p class="subtitle">${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}</p>
|
||||
<p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due'], show_timezone) if section.get('due') is not None else ''}</p>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
@@ -144,10 +144,20 @@
|
||||
<li><div class="icon course-number"></div><p>Course Number</p><span class="course-number">${course.number}</span></li>
|
||||
<li><div class="icon start"></div><p>Classes Start</p><span class="start-date">${course.start_date_text}</span></li>
|
||||
|
||||
## End date should come from course.xml, but this is a quick hack
|
||||
% if get_course_about_section(course, "end_date"):
|
||||
<li><div class="icon end"></div><p>Classes End</p><span class="final-date">${get_course_about_section(course, "end_date")}</span></li>
|
||||
% endif
|
||||
## We plan to ditch end_date (which is not stored in course metadata),
|
||||
## but for backwards compatibility, show about/end_date blob if it exists.
|
||||
% if get_course_about_section(course, "end_date") or course.end:
|
||||
<li>
|
||||
<div class="icon end"></div>
|
||||
<p>Classes End</p><span class="final-date">
|
||||
% if get_course_about_section(course, "end_date"):
|
||||
${get_course_about_section(course, "end_date")}
|
||||
% else:
|
||||
${course.end_date_text}
|
||||
% endif
|
||||
</span>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if get_course_about_section(course, "effort"):
|
||||
<li><div class="icon effort"></div><p>Estimated Effort</p><span class="start-date">${get_course_about_section(course, "effort")}</span></li>
|
||||
|
||||
@@ -156,6 +156,7 @@ function goto( mode)
|
||||
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of answer distributions">
|
||||
<input type="submit" name="action" value="Dump description of graded assignments configuration">
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
@@ -190,6 +191,7 @@ function goto( mode)
|
||||
<input type="submit" name="action" value="Export CSV file of grades for assignment">
|
||||
</li>
|
||||
</ul>
|
||||
<hr width="40%" style="align:left">
|
||||
|
||||
%endif
|
||||
|
||||
@@ -197,11 +199,13 @@ function goto( mode)
|
||||
<p>edX email address or their username: </p>
|
||||
<p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p>
|
||||
<p>and, if you want to reset the number of attempts for a problem, the urlname of that problem</p>
|
||||
<p> <input type="text" name="problem_to_reset"> <input type="submit" name="action" value="Reset student's attempts"> </p>
|
||||
<p> <input type="text" name="problem_to_reset" size="60"> <input type="submit" name="action" value="Reset student's attempts"> </p>
|
||||
|
||||
%if instructor_access:
|
||||
<p> You may also delete the entire state of a student for a problem:
|
||||
<input type="submit" name="action" value="Delete student state for problem"> </p>
|
||||
<p>To delete the state of other XBlocks specify modulename/urlname, eg
|
||||
<tt>combinedopenended/Humanities_SA_Peer</tt></p>
|
||||
%endif
|
||||
|
||||
%endif
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%! from xmodule.util.date_utils import get_default_time_display %>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
|
||||
@@ -29,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
|
||||
|
||||
<section class="course-info">
|
||||
<header>
|
||||
<h1>Course Progress</h1>
|
||||
<h1>Course Progress for Student '${student.username}' (${student.email})</h1>
|
||||
</header>
|
||||
|
||||
%if not course.disable_progress_graph:
|
||||
@@ -60,9 +62,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
|
||||
<p>
|
||||
${section['format']}
|
||||
|
||||
%if 'due' in section and section['due']!="":
|
||||
%if section.get('due') is not None:
|
||||
<em>
|
||||
due ${section['due']}
|
||||
due ${get_default_time_display(section['due'], course.show_timezone)}
|
||||
</em>
|
||||
%endif
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
|
||||
<title>EdX Blog</title>
|
||||
<updated>2013-03-15T14:00:12-07:00</updated>
|
||||
<updated>2013-04-03T14:00:12-07:00</updated>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/17</id>
|
||||
<published>2012-12-19T14:00:00-07:00</published>
|
||||
<updated>2012-12-19T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/>
|
||||
<title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/stanford-university_204x114.png')}" />
|
||||
<p></p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2013:Post/16</id>
|
||||
<published>2013-03-15T10:00:00-07:00</published>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<%!
|
||||
from xmodule.util.date_utils import get_default_time_display
|
||||
%>
|
||||
<div class="folditbasic">
|
||||
<p><strong>Due:</strong> ${due}
|
||||
<p><strong>Due:</strong> ${get_default_time_display(due)}
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
|
||||
@@ -1,428 +1,4 @@
|
||||
[
|
||||
|
||||
{
|
||||
"title": "The Year of the MOOC",
|
||||
"url": "http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html",
|
||||
"author": "Laura Pappano",
|
||||
"image": "nyt_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The New York Times",
|
||||
"publish_date": "November 2, 2012"
|
||||
},
|
||||
{
|
||||
"title": "The Most Important Education Technology in 200 Years",
|
||||
"url": "http://www.technologyreview.com/news/506351/the-most-important-education-technology-in-200-years/",
|
||||
"author": "Antonio Regalado",
|
||||
"image": "techreview_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Technology Review",
|
||||
"publish_date": "November 2, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Classroom in the Cloud",
|
||||
"url": "http://harvardmagazine.com/2012/11/classroom-in-the-cloud",
|
||||
"author": null,
|
||||
"image": "harvardmagazine_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Harvard Magazine",
|
||||
"publish_date": "November-December 2012"
|
||||
},
|
||||
{
|
||||
"title": "How do you stop online students cheating?",
|
||||
"url": "http://www.bbc.co.uk/news/business-19661899",
|
||||
"author": "Sean Coughlan",
|
||||
"image": "bbc_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "BBC",
|
||||
"publish_date": "October 31, 2012"
|
||||
},
|
||||
{
|
||||
"title": "VMware to provide software for HarvardX CS50x",
|
||||
"url": "http://tech.mit.edu/V132/N48/edxvmware.html",
|
||||
"author": "Stan Gill",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Tech",
|
||||
"publish_date": "October 26, 2012"
|
||||
},
|
||||
{
|
||||
"title": "EdX platform integrates into classes",
|
||||
"url": "http://tech.mit.edu/V132/N48/801edx.html",
|
||||
"author": "Leon Lin",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Tech",
|
||||
"publish_date": "October 26, 2012"
|
||||
},
|
||||
{
|
||||
"title": "VMware Offers Free Software to edX Learners",
|
||||
"url": "http://campustechnology.com/articles/2012/10/25/vmware-offers-free-virtualization-software-for-edx-computer-science-students.aspx",
|
||||
"author": "Joshua Bolkan",
|
||||
"image": "campustech_logo_178x138.jpg",
|
||||
"deck": "VMware Offers Free Virtualization Software for EdX Computer Science Students",
|
||||
"publication": "Campus Technology",
|
||||
"publish_date": "October 25, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Lone Star moots charges to make Moocs add up",
|
||||
"url": "http://www.timeshighereducation.co.uk/story.asp?sectioncode=26&storycode=421577&c=1",
|
||||
"author": "David Matthews",
|
||||
"image": "timeshighered_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Times Higher Education",
|
||||
"publish_date": "October 25, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Free, high-quality and with mass appeal",
|
||||
"url": "http://www.ft.com/intl/cms/s/2/73030f44-d4dd-11e1-9444-00144feabdc0.html#axzz2A9qvk48A",
|
||||
"author": "Rebecca Knight",
|
||||
"image": "ft_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Financial Times",
|
||||
"publish_date": "October 22, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Getting the most out of an online education",
|
||||
"url": "http://www.reuters.com/article/2012/10/19/us-education-courses-online-idUSBRE89I17120121019",
|
||||
"author": "Kathleen Kingsbury",
|
||||
"image": "reuters_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Reuters",
|
||||
"publish_date": "October 19, 2012"
|
||||
},
|
||||
{
|
||||
"title": "EdX announces partnership with Cengage",
|
||||
"url": "http://tech.mit.edu/V132/N46/cengage.html",
|
||||
"author": "Leon Lin",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Tech",
|
||||
"publish_date": "October 19, 2012"
|
||||
},
|
||||
{
|
||||
"title": "U Texas System Joins EdX",
|
||||
"url": "http://campustechnology.com/articles/2012/10/18/u-texas-system-joins-edx.aspx",
|
||||
"author": "Joshua Bolkan",
|
||||
"image": "campustech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Campus Technology",
|
||||
"publish_date": "October 18, 2012"
|
||||
},
|
||||
{
|
||||
"title": "San Jose State University Runs Blended Learning Course Using edX ",
|
||||
"url": "http://chronicle.com/blogs/wiredcampus/san-jose-state-u-says-replacing-live-lectures-with-videos-increased-test-scores/40470",
|
||||
"author": "Alisha Azevedo",
|
||||
"image": "chroniclehighered_logo_178x138.jpeg",
|
||||
"deck": "San Jose State U. Says Replacing Live Lectures With Videos Increased Test Scores",
|
||||
"publication": "Chronicle of Higher Education",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Online university to charge tuition fees",
|
||||
"url": "http://www.bbc.co.uk/news/education-19964787",
|
||||
"author": "Sean Coughlan",
|
||||
"image": "bbc_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "BBC",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "HarvardX marks the spot",
|
||||
"url": "http://news.harvard.edu/gazette/story/2012/10/harvardx-marks-the-spot/",
|
||||
"author": "Tania delLuzuriaga",
|
||||
"image": "harvardgazette_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Harvard Gazette",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard EdX Enrolls Near 100000 Students for Free Online Classes",
|
||||
"url": "http://www.collegeclasses.com/harvard-edx-enrolls-near-100000-students-for-free-online-classes/",
|
||||
"author": "Keith Koong",
|
||||
"image": "college_classes_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "CollegeClasses.com",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Cengage Learning to Provide Book Content and Pedagogy through edX's Not-for-Profit Interactive Study Via the Web",
|
||||
"url": "http://www.outsellinc.com/our_industry/headlines/1087978",
|
||||
"author": null,
|
||||
"image": "outsell_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Outsell.com",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas System Embraces MOOCs",
|
||||
"url": "http://www.usnewsuniversitydirectory.com/articles/university-of-texas-system-embraces-moocs_12713.aspx#.UIBLVq7bNzo",
|
||||
"author": "Chris Hassan",
|
||||
"image": "usnews_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "US News",
|
||||
"publish_date": "October 17, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Texas MOOCs for Credit?",
|
||||
"url": "http://www.insidehighered.com/news/2012/10/16/u-texas-aims-use-moocs-reduce-costs-increase-completion",
|
||||
"author": "Steve Kolowich",
|
||||
"image": "insidehighered_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Insider Higher Ed",
|
||||
"publish_date": "October 16, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas Joins Harvard-Founded edX",
|
||||
"url": "http://www.thecrimson.com/article/2012/10/16/University-of-Texas-edX/",
|
||||
"author": "Kevin J. Wu",
|
||||
"image": "harvardcrimson_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The Crimson",
|
||||
"publish_date": "October 16, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Entire UT System to join edX",
|
||||
"url": "http://tech.mit.edu/V132/N45/edx.html",
|
||||
"author": "Ethan A. Solomon",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Tech",
|
||||
"publish_date": "October 16, 2012"
|
||||
},
|
||||
{
|
||||
"title": "First University System Joins edX Platform",
|
||||
"url": "http://www.govtech.com/education/First-University-System-Joins-edX-platform.html",
|
||||
"author": "Tanya Roscoria",
|
||||
"image": "govtech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "GovTech.com",
|
||||
"publish_date": "October 16, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas Joining Harvard, MIT Online Venture",
|
||||
"url": "http://www.bloomberg.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture.html",
|
||||
"author": "David Mildenberg",
|
||||
"image": "bloomberg_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Bloomberg",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas Joining Harvard, MIT Online Venture",
|
||||
"url": "http://www.businessweek.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture",
|
||||
"author": "David Mildenberg",
|
||||
"image": "busweek_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Business Week",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Univ. of Texas joins online course program edX",
|
||||
"url": "http://news.yahoo.com/univ-texas-joins-online-course-program-edx-172202035--finance.html",
|
||||
"author": "Chris Tomlinson",
|
||||
"image": "ap_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Associated Press",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "U. of Texas Plans to Join edX",
|
||||
"url": "http://www.insidehighered.com/quicktakes/2012/10/15/u-texas-plans-join-edx",
|
||||
"author": null,
|
||||
"image": "insidehighered_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Inside Higher Ed",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "U. of Texas System Is Latest to Sign Up With edX for Online Courses",
|
||||
"url": "http://chronicle.com/blogs/wiredcampus/u-of-texas-system-is-latest-to-sign-up-with-edx-for-online-courses/40440",
|
||||
"author": "Alisha Azevedo",
|
||||
"image": "chroniclehighered_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Chronicle of Higher Education",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "First University System Joins edX",
|
||||
"url": "http://www.centerdigitaled.com/news/First-University-System-Joins-edX.html",
|
||||
"author": "Tanya Roscoria",
|
||||
"image": "center_digeducation_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Center for Digital Education",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas Joins Harvard, MIT in edX Online Learning Venture",
|
||||
"url": "http://harvardmagazine.com/2012/10/university-of-texas-joins-harvard-mit-edx",
|
||||
"author": null,
|
||||
"image": "harvardmagazine_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Harvard Magazine",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas joins edX",
|
||||
"url": "http://www.masshightech.com/stories/2012/10/15/daily13-University-of-Texas-joins-edX.html",
|
||||
"author": "Don Seiffert",
|
||||
"image": "masshightech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "MassHighTech",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "UT System to Forge Partnership with EdX",
|
||||
"url": "http://www.texastribune.org/texas-education/higher-education/ut-system-announce-partnership-edx/",
|
||||
"author": "Reeve Hamilton",
|
||||
"image": "texastribune_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Texas Tribune",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "UT System puts $5 million into online learning initiative",
|
||||
"url": "http://www.statesman.com/news/news/local/ut-system-puts-5-million-into-online-learning-init/nSdd5/",
|
||||
"author": "Ralph K.M. Haurwitz",
|
||||
"image": "austin_statesman_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Austin Statesman",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard’s Online Classes Sound Pretty Popular",
|
||||
"url": "http://blogs.bostonmagazine.com/boston_daily/2012/10/15/harvards-online-classes-sound-pretty-popular/",
|
||||
"author": "Eric Randall",
|
||||
"image": "bostonmag_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Boston Magazine",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard Debuts Free Online Courses",
|
||||
"url": "http://www.ibtimes.com/harvard-debuts-free-online-courses-846629",
|
||||
"author": "Eli Epstein",
|
||||
"image": "ibtimes_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "International Business Times",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "UT System Joins Online Learning Effort",
|
||||
"url": "http://www.texastechpulse.com/ut_system_joins_online_learning_effort/s-0045632.html",
|
||||
"author": null,
|
||||
"image": "texaspulse_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Texas Tech Pulse",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "University of Texas Joins edX",
|
||||
"url": "http://www.onlinecolleges.net/2012/10/15/university-of-texas-joins-edx/",
|
||||
"author": "Alex Wukman",
|
||||
"image": "online_colleges_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Online Colleges.net",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "100,000 sign up for first Harvard online courses",
|
||||
"url": "http://www.masslive.com/news/index.ssf/2012/10/100000_sign_up_for_first_harva.html",
|
||||
"author": null,
|
||||
"image": "ap_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Associated Press",
|
||||
"publish_date": "October 15, 2012"
|
||||
},
|
||||
{
|
||||
"title": "In the new Listener, on sale from 14.10.12",
|
||||
"url": "http://www.listener.co.nz/commentary/the-internaut/in-the-new-listener-on-sale-from-14-10-12/",
|
||||
"author": null,
|
||||
"image": "nz_listener_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Listener",
|
||||
"publish_date": "October 14, 2012"
|
||||
},
|
||||
{
|
||||
"title": "HarvardX Classes to Begin Tomorrow",
|
||||
"url": "http://www.thecrimson.com/article/2012/10/14/harvardx-classes-start-tomorrow/",
|
||||
"author": "Hana N. Rouse",
|
||||
"image": "harvardcrimson_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The Crimson",
|
||||
"publish_date": "October 14, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Online Harvard University courses draw well",
|
||||
"url": "http://bostonglobe.com/metro/2012/10/14/harvard-launching-free-online-courses-sign-for-first-two-classes/zBDuHY0zqD4OESrXWfEgML/story.html",
|
||||
"author": "Brock Parker",
|
||||
"image": "bostonglobe_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Boston Globe",
|
||||
"publish_date": "October 14, 2012"
|
||||
},
|
||||
{
|
||||
"title": "Harvard ready to launch its first free online courses Monday",
|
||||
"url": "http://www.boston.com/yourtown/news/cambridge/2012/10/harvard_ready_to_launch_its_fi.html",
|
||||
"author": "Brock Parker",
|
||||
"image": "bostonglobe_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Boston Globe",
|
||||
"publish_date": "October 12, 2012"
|
||||
},
|
||||
{
|
||||
"title": "edX: Harvard's New Domain",
|
||||
"url": "http://www.thecrimson.com/article/2012/10/4/edx-scrutiny-online-learning/ ",
|
||||
"author": "Delphine Rodrik and Kevin Su",
|
||||
"image": "harvardcrimson_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "The Crimson",
|
||||
"publish_date": "October 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "New Experiments in the edX Higher Ed Petri Dish",
|
||||
"url": "http://www.nonprofitquarterly.org/policysocial-context/21116-new-experiments-in-the-edx-higher-ed-petri-dish.html",
|
||||
"author": "Michelle Shumate",
|
||||
"image": "npq_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Non-Profit Quarterly",
|
||||
"publish_date": "October 4, 2012"
|
||||
},
|
||||
{
|
||||
"title": "What Campuses Can Learn From Online Teaching",
|
||||
"url": "http://online.wsj.com/article/SB10000872396390444620104578012262106378182.html?mod=googlenews_wsj",
|
||||
"author": "Rafael Reif",
|
||||
"image": "wsj_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Wall Street Journal",
|
||||
"publish_date": "October 2, 2012"
|
||||
},
|
||||
{
|
||||
"title": "MongoDB courses to be offered via edX",
|
||||
"url": "http://tech.mit.edu/V132/N42/edxmongodb.html",
|
||||
"author": "Jake H. Gunter",
|
||||
"image": "thetech_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "The Tech",
|
||||
"publish_date": "October 2, 2012"
|
||||
},
|
||||
{
|
||||
"title": "5 Ways That edX Could Change Education",
|
||||
"url": "http://chronicle.com/article/5-Ways-That-edX-Could-Change/134672/",
|
||||
"author": "Marc Parry",
|
||||
"image": "chroniclehighered_logo_178x138.jpeg",
|
||||
"deck": null,
|
||||
"publication": "Chronicle of Higher Education",
|
||||
"publish_date": "October 1, 2012"
|
||||
},
|
||||
{
|
||||
"title": "MIT profs wait to teach you, for free",
|
||||
"url": "http://www.dnaindia.com/mumbai/report_mit-profs-wait-to-teach-you-for-free_1747273",
|
||||
"author": "Kanchan Srivastava",
|
||||
"image": "dailynews_india_logo_178x138.jpg",
|
||||
"deck": null,
|
||||
"publication": "Daily News and Analysis India",
|
||||
"publish_date": "October 1, 2012"
|
||||
},
|
||||
{
|
||||
"title": "The Year of the MOOC",
|
||||
"url": "http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html",
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
## The JS for this is defined in xqa_interface.html
|
||||
${module_content}
|
||||
%if location.category in ['problem','video','html']:
|
||||
%if location.category in ['problem','video','html','combinedopenended']:
|
||||
% if edit_link:
|
||||
<div>
|
||||
<a href="${edit_link}">Edit</a> /
|
||||
<a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
|
||||
<a href="${edit_link}">Edit</a>
|
||||
% if xqa_key:
|
||||
/ <a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
|
||||
'location': '${location}',
|
||||
'xqa_key': '${xqa_key}',
|
||||
'category': '${category}',
|
||||
'user': '${user}'
|
||||
})" id="${element_id}_xqa_log">QA</a>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
<div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
|
||||
@@ -61,6 +63,12 @@ location = ${location | h}
|
||||
<tr><td>${name}</td><td><pre style="display:inline-block; margin: 0;">${field | h}</pre></td></tr>
|
||||
%endfor
|
||||
</table>
|
||||
<table>
|
||||
<tr><th>XML attributes</th></tr>
|
||||
%for name, field in xml_attributes.items():
|
||||
<tr><td>${name}</td><td><pre style="display:inline-block; margin: 0;">${field | h}</pre></td></tr>
|
||||
%endfor
|
||||
</table>
|
||||
category = ${category | h}
|
||||
</div>
|
||||
%if render_histogram:
|
||||
|
||||
@@ -73,45 +73,10 @@
|
||||
</article>
|
||||
-->
|
||||
|
||||
<article id="associate-legal-counsel" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>ASSOCIATE LEGAL COUNSEL</strong></h3>
|
||||
|
||||
<p>We are seeking a talented lawyer with the ability to operate independently in a fast-paced environment and work proactively with all members of the edX team. You must have thorough knowledge of intellectual property law, contracts and licensing. </p>
|
||||
|
||||
<p><strong>Key Responsibilities: </strong></p>
|
||||
<ul>
|
||||
<li>Drive the negotiating, reviewing, drafting and overseeing of a wide range of transactional arrangements, including collaborations related to the provision of online education, inbound and outbound licensing of intellectual property, strategic partnerships, nondisclosure agreements, and services agreements.</li>
|
||||
<li>Provide counseling on the legal implications/considerations of business and technical strategies and projects, with special emphasis on regulations related to higher education, data security and privacy.</li>
|
||||
<li>Provide advice and support company-wide on a variety of legal issues in a timely and effective manner.</li>
|
||||
<li>Assist on other matters as needed.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Requirements:</strong></p>
|
||||
|
||||
<ul>
|
||||
<li>JD from an accredited law school</li>
|
||||
<li>Massachusetts bar admission required</li>
|
||||
<li>2-3 years of transactional experience at a major law firm and/or as an in-house counselor</li>
|
||||
<li>Substantial IP licensing experience</li>
|
||||
<li>Knowledge of copyright, trademark and patent law</li>
|
||||
<li>Experience with open source content and open source software preferred</li>
|
||||
<li>Outstanding communications skills (written and oral)</li>
|
||||
<li>Experience with drafting and legal review of Internet privacy policies and terms of use.</li>
|
||||
<li>Understanding of how to balance legal risks with business objectives</li>
|
||||
<li>Ability to develop an innovative approach to legal issues in support of strategic business initiatives</li>
|
||||
<li>An internal business and customer focused proactive attitude with ability to prioritize effectively</li>
|
||||
<li>Experience with higher education preferred but not required</li>
|
||||
</ul>
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="director-of-education-services" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
|
||||
<p>The edX Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:</p>
|
||||
<h3><strong>VICE PRESIDENT/DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
|
||||
<p>The edX VP/Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:</p>
|
||||
<ol>
|
||||
<li>Delivering 20 new courses in 2013 in collaboration with the partner Universities
|
||||
<ul>
|
||||
@@ -194,13 +159,50 @@
|
||||
<li>Proactive, optimistic approach to problem solving.</li>
|
||||
<li>Commitment to constant personal and organizational improvement.</li>
|
||||
<li>Willingness to travel to partner sites as needed. </li>
|
||||
<li>Bachelors required, Master’s in Education, organizational learning, or other related field preferred. </li>
|
||||
<li>Bachelor's or Master’s in Education, organizational learning, or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="trainer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>TRAINER</strong></h3>
|
||||
<p>All those Universities on the edX homepage are full of incredible professors and teaching teams, designing on-line courses that will change the face of education. The edX team is constantly training whole new university teams on how to make their visions shine using edX software. We’re looking for some truly talented people to help train people on the tools that are enabling the future of education.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Facilitate training programs as required ensuring that best practices are incorporated in all learning environments.</li>
|
||||
<li>Create and design learning materials for training curriculums, incorporate edX best practices into training curriculum.</li>
|
||||
<li>Incorporate key performance metrics into training modules; participate in strategic initiatives</li>
|
||||
<li>Measure, monitor and share training results with business units to identify future training opportunities.</li>
|
||||
<li>Identify and leverage existing resources to maximize partner efficiency and productivity. </li>
|
||||
<li>Work with both Universities and edX to provide strategic input based on future training needs.</li>
|
||||
<li>Communicate effectively in oral and written presentations.</li>
|
||||
<li>Analyze learners training needs and identify cross training opportunities.</li>
|
||||
<li>Mentor and train others on training tools to expand training efficiency and uniformity.</li>
|
||||
<li>Build relationships with universities to be viewed as a trusted training partner. </li>
|
||||
</ul>
|
||||
<p><strong>Requirements:</strong></p>
|
||||
<ul>
|
||||
<li>Minimum of 1-3 years experience developing and delivering educational training, preferably in an educational technology organization. </li>
|
||||
<li>Lean and Agile thinking and training. Experienced in Scrum or kanban preferred.</li>
|
||||
<li>Excellent interpersonal skills including proven presentation and facilitation skills.</li>
|
||||
<li>Strong oral and written communication skills.</li>
|
||||
<li>Flexibility to work on a variety of initiatives; prior startup experience preferred.</li>
|
||||
<li>Outstanding work ethic, results-oriented, and creative/innovative style.</li>
|
||||
<li>Proactive, optimistic approach to problem solving.</li>
|
||||
<li>Commitment to constant personal and organizational improvement.</li>
|
||||
<li>Willingness to travel to partner sites as needed.</li>
|
||||
<li>Bachelors or Master’s in Education, organizational learning, instructional design or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="instructional-designer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>INSTRUCTIONAL DESIGNER</strong></h3>
|
||||
@@ -222,8 +224,10 @@
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.</li>
|
||||
<li>Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.</li> Ability to meet deadlines and manage expectations of constituents.
|
||||
<li>Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
<li>Experience in higher education with additional experience in a start-up or research environment preferable.</li>
|
||||
<li>Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.</li>
|
||||
<li>Ability to meet deadlines and manage expectations of constituents.</li>
|
||||
<li>Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.</li>
|
||||
<li>Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.</li>
|
||||
</ul>
|
||||
@@ -240,16 +244,16 @@
|
||||
<p>edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple</li> teams engaged in the production of the courses assigned to them.
|
||||
<li>Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple teams engaged in the production of the courses assigned to them.</li>
|
||||
<li>Train partners and drive best practices adoption. PMs train course staff from partner institutions and help them adopt best practices for workflow and tools. </li>
|
||||
<li>Build capacity. Mentor staff at partner institutions, train the trainers that help them scale their course production ability.</li>
|
||||
<li>Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and</li> Kanban, and are Lean thinkers and educators.
|
||||
<li>Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and Kanban, and are Lean thinkers and educators.</li>
|
||||
<li>Improve workflows. PMs are responsible for carefully assessing the methods and outputs of each course and adjusting them to take best advantage of available resources.</li>
|
||||
<li>Encourage innovation. Spark creativity in course teams to build new courses that could never be produced in brick and mortar settings.</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Bachelor's Degree. Master's Degree preferred.</li>
|
||||
<li>Bachelor's Degree. Master's Degree preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
<li>At least 2 years of experience working with University faculty and administrators.</li>
|
||||
<li>Proven record of successful Scrum or Kanban project management, including use of project management tools. </li>
|
||||
<li>Ability to create processes that systematically provide solutions to open ended challenges.</li>
|
||||
@@ -272,36 +276,6 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="project-manager-pmo" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>PROJECT MANAGER (PMO)</strong></h3>
|
||||
<p>As a fast paced, rapidly growing organization serving the evolving online higher education market, edX maximizes its talents and resources. To help make the most of this unapologetically intelligent and dedicated team, we seek a project manager to increase the accuracy of our resource and schedule estimates and our stakeholder satisfaction.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Coordinate multiple projects to bring Courses, Software Product and Marketing initiatives to market, all of which are related, which have both dedicated and shared resources.</li>
|
||||
<li>Provide, at a moment’s notice, the state of development, so that priorities can be enforced or reset, so that future expectations can be set accurately.</li>
|
||||
<li>Develop lean processes that supports a wide variety of efforts which draw on a shared resource pool.</li>
|
||||
<li>Develop metrics on resource use that support the leadership team in optimizing how they respond to unexpected challenges and new opportunities.</li>
|
||||
<li>Accurately and clearly escalate only those issues which need escalation for productive resolution. Assist in establishing consensus for all other issues.</li>
|
||||
<li>Advise the team on best practices, whether developed internally or as industry standards.</li>
|
||||
<li>Recommend to the leadership team how to re-deploy key resources to better match stated priorities.</li>
|
||||
<li>Help the organization deliver on its commitments with more consistency and efficiency. Allow the organization to respond to new opportunities with more certainty in its ability to forecast resource needs.</li>
|
||||
<li>Select and maintain project management tools for Scrum and Kanban that can serve as the standard for those we use with our partners.</li>
|
||||
<li>Forecast future resource needs given the strategic direction of the organization.</li>
|
||||
</ul>
|
||||
<p><strong>Skills:</strong></p>
|
||||
<ul>
|
||||
<li>Bachelor’s degree or higher</li>
|
||||
<li>Exquisite communication skills, especially listening</li>
|
||||
<li>Inexhaustible attention to detail with the ability to let go of perfection</li>
|
||||
<li>Deep commitment to Lean project management, including a dedication to its best intentions not just its rituals</li>
|
||||
<li>Sense of humor and humility</li>
|
||||
<li>Ability to hold on to the important in the face of the urgent</li>
|
||||
</ul>
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="director-of-product-management" class="job">
|
||||
<div class="inner-wrapper">
|
||||
@@ -321,8 +295,7 @@
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Bachelor’s degree or higher in a Technical Area</li>
|
||||
<li>MBA or Masters in Design preferred</li>
|
||||
<li>Bachelor’s degree or higher in a Technical Area, MBA or Masters in Design preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
<li>Proven ability to develop and implement strategy</li>
|
||||
<li>Exquisite organizational skills</li>
|
||||
<li>Deep analytical skills</li>
|
||||
@@ -354,7 +327,7 @@
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Bachelor’s degree or higher</li>
|
||||
<li>Bachelor’s degree or higher. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
<li>Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js</li>
|
||||
<li>Ability to work on multiple projects simultaneously without splintering</li>
|
||||
<li>Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.</li>
|
||||
@@ -369,40 +342,6 @@
|
||||
</article>
|
||||
|
||||
|
||||
<article id="director-engineering-open-source" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER</strong></h3>
|
||||
<p>In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Define and implement software design standards that make the open source community most welcome and productive.</li>
|
||||
<li>Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to</li> make the edX platform the world’s best learning platform.
|
||||
<li>Help the organization recognize the benefits and limitations inherent in open source solutions.</li>
|
||||
<li>Establish best practices and key tool usage, especially those based on industry standards.</li>
|
||||
<li>Provide visibility for the leadership team into the concerns and challenges faced by the open source community.</li>
|
||||
<li>Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.</li>
|
||||
<li>Maximize the good code design coming from the open source community.</li>
|
||||
<li>Provide the wit and firmness that the community needs to channel their energy productively.</li>
|
||||
<li>Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.</li>
|
||||
<li>Shorten lines of communication and build trust across entire team</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
|
||||
<li>Bachelors, preferably Masters in Computer Science</li>
|
||||
<li>Solid communication skills, especially written</li>
|
||||
<li>Committed to Agile practice, Scrum and Kanban</li>
|
||||
<li>Charm and humor</li>
|
||||
<li>Deep familiarity with Open Source, participant and contributor</li>
|
||||
<li>Python, Django, Javascript</li>
|
||||
<li>Commitment to support your technical recommendations, both within and beyond the organization.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="software-engineer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>SOFTWARE ENGINEER</strong></h3>
|
||||
@@ -427,7 +366,7 @@
|
||||
<li>Test Driven Development</li>
|
||||
<li>Committed to Documentation best practices so your code can be consumed in an open source environment</li>
|
||||
<li>Contributor to or consumer of Open Source Frameworks</li>
|
||||
<li>BS in Computer Science from top-tier institution</li>
|
||||
<li>BS in Computer Science from top-tier institution. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
<li>Acknowledged by peers as a technology leader </li>
|
||||
</ul>
|
||||
|
||||
@@ -436,21 +375,130 @@
|
||||
</article>
|
||||
|
||||
|
||||
<article id="devops-engineer-systems-administrator" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>DEVOPS ENGINEER – SYESTEMS ADMINISTRATOR</strong></h3>
|
||||
<p>The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.</p>
|
||||
<p>In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Work with developers and staff to maintain and improve the infrastructure of edX.</li>
|
||||
<li>Assist where needed with other technical support tasks to support the fast moving pace of edX.</li>
|
||||
<li>Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.</li>
|
||||
</ul>
|
||||
<p><strong>Requirements:</strong></p>
|
||||
<ul>
|
||||
<li>Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.3 or more years of systems administration. </li>
|
||||
<li>Must have an excellent working knowledge of Linux both as an end-user and as an administrator.</li>
|
||||
<li>Must be adept in programming/scripting languages such as Python, Ruby, Bash.</li>
|
||||
<li>Must be familiar with a configuration management system such as Puppet, Chef, Ansible.</li>
|
||||
<li>Must have experience running web applications in a production environment.</li>
|
||||
<li>Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.</li>
|
||||
<li>Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
<article id="learning-sciences-engineer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>LEARNING SCIENCES ENGINEER</strong></h3>
|
||||
<p>In 2012, edX reinvented education. In 2013, the edX learning sciences team is charged with reinventing education, again. The goal of the team is to prototype and develop technologies which will radically change the way students learn and instructors teach. We will engage in projects in learning analytics, crowdsourced content development, intelligent tutoring, as well as radical changes to the ways course content is structured. We are looking to opportunistically build a small (3 person), fast-moving team capable of rapidly bringing advanced development projects to prototype and to market. All members of the team must be spectacular software engineers capable of working in or adapting to dynamic, duck typed, functional languages (Python and JavaScript). In addition, we are looking for some combination of:</p>
|
||||
<ul>
|
||||
<li>Deep expertise in mathematics, and in particular, advanced linear algebra, machine learning, big data, psychometrics, and probability. </li>
|
||||
<li>UX design. Capable of envisioning user interface for software that does things that have never been done before, and bringing them through to market. Skills should be broad and range the full gamut: graphic design, UX, HTML5, basic JavaScript, and CSS. </li>
|
||||
<li>Interest and experience in both research and practice of education, cognitive science, and related fields. </li>
|
||||
<li>Core backend experience (Python, Django, MongoDB, SQL)</li>
|
||||
<li>Background in social networks and social network analysis (both social science and mathematics) is desirable as well.</li>
|
||||
</ul>
|
||||
<p>More than anything, we’re looking for spectacular people capable of very rapidly building things which have never been built before. We’re capable of providing both traditional employment, and potentially, in partnership with MIT, more academic opportunities.</p>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="sales-engineer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>SALES ENGINEER, BUSINESS DEVELOPMENT TEAM</strong></h3>
|
||||
<p>A great relationship with edX begins long before the first student signs up. We are
|
||||
looking for some talented, self-motivated people to help set a solid foundation for
|
||||
our emerging corporate customers, NGO’s, and governmental partners. As the Sales
|
||||
Engineer you will be expected to provide oversight if requested over more junior
|
||||
staff. This may include skills development, knowledge transfer, sharing technical
|
||||
expertise, and review of demos prior to presentation. The Sales Engineer should
|
||||
have familiarity with instructional design and competency in that area is a plus.</p>
|
||||
|
||||
<p>Experience teaching and mentoring, needs assessment and prior management
|
||||
responsibility, and LMS experience, also a plus. This is a team atmosphere with
|
||||
many constituencies working to develop a new global perspective regarding higher
|
||||
education and online learning. Respect and patience, along with knowledge and
|
||||
understanding of the development process is critical to success in order to maintain
|
||||
strong bonds between development teams, sales team, and prospect/client
|
||||
implementation teams. In addition the Sales Engineer may also work with our
|
||||
xUniversity partners and the affiliated professors joining the edX movement. This
|
||||
position requires customer facing skills, comfort in demonstrating the product, and
|
||||
ability to code ‘demos’ as required. Additionally you will be contributing to
|
||||
proposals, so clear documentation and writing skills are critical. The job will
|
||||
require travel to client sites around the US upon occasion, and possibly
|
||||
internationally as well. Job also requires good speaking skills, and a willingness and
|
||||
ability to communicate clearly and respond quickly to prospect and customer
|
||||
requests. This is a salaried position and will on occasion require work and
|
||||
responsiveness to both the edX team and customers ‘after hours’. This position
|
||||
reports to the VP, Business Development and will be dotted lined to the
|
||||
development and program management teams.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Can code demos and evaluate demos of others</li>
|
||||
<li>Prepare and deliver standard and custom demonstrations</li>
|
||||
<li>Handle all pre-sales technical issues professionally and efficiently</li>
|
||||
<li>Maintain in-depth knowledge of products and pending new releases</li>
|
||||
<li>Maintain a working knowledge of documentation and training</li>
|
||||
<li>Maintain a working knowledge of workflow systems</li>
|
||||
<li>Respond to technical questions from universities looking to expand their on-line offerings</li>
|
||||
<li>Provide feedback to Product Development regarding new features, improving product performance, and eliminating bugs in the product</li>
|
||||
<li>Prepare Professional Services for efficient onboarding – professionally managing the transition from pre-sales to post-sales</li>
|
||||
<li>Deliver high-level presentation and associated ‘click-thru’ demonstrations</li>
|
||||
<li>and be able to customize to prospect’s requirements</li>
|
||||
<li>Understand and articulate the underlying technology concepts</li>
|
||||
<li>Understand and articulate how all products components fit together technically as well as how they integrate and work with external technologies and cross functional applications found within clients organizations.</li>
|
||||
<li>Build relationships with our prospects and universities, to be viewed as a trusted training partner.</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Minimum of 5 years of experience working closely with relationship based sales organizations, preferably in an educational technology organization.</li>
|
||||
<li>Excellent interpersonal skills including proven presentation and facilitation skills.</li>
|
||||
<li>Strong oral and written communication skills.</li>
|
||||
<li>Flexibility to work on a variety of initiatives; prior startup experience preferred.</li>
|
||||
<li>Outstanding work ethic, results-oriented, and creative/innovative style.</li>
|
||||
<li>Proactive, optimistic approach to problem solving.</li>
|
||||
<li>Commitment to constant personal and organizational improvement.</li>
|
||||
<li>Willingness to travel to partner sites as needed.</li>
|
||||
<li>Lean and Agile thinking and training. Experienced in Scrum or kanban.</li>
|
||||
<li>Bachelors or Master’s in Education, organizational learning, or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="jobs-sidebar">
|
||||
<h2>Positions</h2>
|
||||
<nav>
|
||||
<a href="#associate-legal-counsel">Associate Legal Counsel</a>
|
||||
<a href="#director-of-education-services">Director of Education Services</a>
|
||||
<a href="#director-of-education-services">Vice President/Director of Education Services</a>
|
||||
<a href="#manager-of-training-services">Manager of Training Services</a>
|
||||
<a href="#trainer">Trainer</a>
|
||||
<a href="#instructional-designer">Instructional Designer</a>
|
||||
<a href="#program-manager">Program Manager</a>
|
||||
<a href="#project-manager-pmo">Project Manager (PMO)</a>
|
||||
<a href="#director-of-product-management">Director of Product Management</a>
|
||||
<a href="#content-engineer">Content Engineer</a>
|
||||
<a href="#director-engineering-open-source">Director Engineering, Open Source Community Manager</a>
|
||||
<a href="#software-engineer">Software Engineer</a>
|
||||
<a href="#devops-engineer-systems-administrator">Devops Engineer - Systems Administrator</a>
|
||||
<a href="#learning-sciences-engineer">Learning Sciences Engineer</a>
|
||||
<a href="#sales-engineer">Sales Engineer, Business Development Team</a>
|
||||
</nav>
|
||||
<h2>How to Apply</h2>
|
||||
<p>E-mail your resume, cover letter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../../main.html" />
|
||||
|
||||
<%namespace name='static' file='../../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title></%block>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<section class="pressrelease">
|
||||
<section class="container">
|
||||
<h1>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</h1>
|
||||
<hr class="horizontal-divider">
|
||||
<article>
|
||||
<h2>edX Learning Platform to be open source and available on June 1</h2>
|
||||
|
||||
<p><strong>CAMBRIDGE, MA and STANFORD, CA – April 3, 2013 –</strong>
|
||||
|
||||
Stanford University and <a href="https://www.edx.org">edX</a>, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced their collaboration to advance the development of edX’s open source learning platform and provide free and open online learning tools for institutions around the world.</p>
|
||||
|
||||
<p>As part of this announcement, edX will release the source code for its entire online learning platform on June 1, 2013. In support of that move, Stanford will integrate features of its existing Class2Go platform into the edX platform, use the integration as an internal platform for online coursework for on-campus and distance learners, and work collaboratively with edX and other institutions to further develop the edX platform.</p>
|
||||
|
||||
<p>“This collaboration brings together two leaders in online education in a common effort to ensure that the world’s universities have the strongest possible not-for-profit, open source platform available to them,” said John Mitchell, vice provost for online learning at Stanford University. “A not-for-profit, open source platform will help universities experiment with different ways to produce and share content, fostering continued innovation through a vibrant community of contributors.”</p>
|
||||
|
||||
<p>EdX and Stanford will collaborate along with others around the globe on the ongoing development and refinement of the edX online learning platform. As of June 1, developers everywhere will be able to freely access the source code of the edX learning platform, including code for its Learning Management System (LMS); Studio, a course authoring tool; xBlock, an application programming interface (API) for integrating third-party learning objects; and machine grading API’s. EdX will support and nurture the community of developers contributing to the enhancement of the edX platform by providing a rich environment for developer collaboration as well as technical and process guidelines to facilitate developer contributions.</p>
|
||||
|
||||
<p>“It has been our vision to offer our platform as open source since edX’s founding by Harvard and MIT,” stated Anant Agarwal, president of edX. “We are now realizing that vision, and I am pleased to welcome Stanford University, one of the world’s leading institutions of higher education, to further this global open source solution. I want to acknowledge the key role played by our X Consortium member UC Berkeley, which was instrumental in fostering this collaboration. We believe the edX platform—the Linux of learning—will benefit from all the world’s institutions and communities.”</p>
|
||||
|
||||
<p>EdX is pursuing an open source vision to enhance access to higher education for the entire world. One of the chief benefits of massive open online courses (MOOCs) is that they bring together a tremendously diverse student body to learn with and from each other. EdX has chosen to extend that perspective to its learning platform as well, knowing that drawing upon the global community of developers is an effective route to both transform and deliver the world’s best and most accessible online and blended learning experience.</p>
|
||||
|
||||
<p>MOOCs and innovative online teaching approaches on college campuses, such as the “flipped classroom,” use web environments that support interactive video, online discussion, social/cohort interaction, assessment and other functions. Open source online learning platforms will allow universities to develop their own delivery methods, partner with other universities and institutions as they choose, collect data, and control branding of their educational material. Further developing online opportunities through open source technology is a key objective of the partnership between edX and Stanford.</p>
|
||||
|
||||
<p>Stanford will continue to provide a range of platforms for its instructors to choose from in hosting their online coursework, including continued partnerships with Coursera and other providers. The university will focus its ongoing platform development efforts on the new platform, combining key features from the Class2Go open source platform with the open source edX code base.</p>
|
||||
|
||||
<p>The edX learning platform source code, as well as platform developments from Stanford, edX and other contributors, will be available on June 1, 2013 and can be accessed from the edX Platform Repository located at <a href="https://github.com/edX">https://github.com/edX</a>.</p>
|
||||
|
||||
|
||||
<h2>About edX</h2>
|
||||
|
||||
<p><a href="https://www.edx.org/">EdX</a> is a not-for-profit enterprise of its founding partners <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.</p>
|
||||
|
||||
<h2>About Stanford University</h2>
|
||||
|
||||
<p>
|
||||
<a href="http://www.stanford.edu">Stanford University</a> is engaged in a variety of efforts to develop online learning – experimenting with coursework for both on-campus and off-campus students, researching key questions around what a digital environment means for teaching and learning, and pursuing platform development. More information on Stanford’s online learning activities is available at <a href="http://online.stanford.edu">http://online.stanford.edu</a>
|
||||
|
||||
|
||||
<section class="contact">
|
||||
<p><strong>Media Contact:</strong></p>
|
||||
<p>Dan O'Connell</p>
|
||||
<p>oconnell@edx.org</p>
|
||||
<p>(617) 480-6585</p>
|
||||
</section>
|
||||
|
||||
<section class="contact">
|
||||
<p>Brad Hayward</p>
|
||||
<p>bhayward@stanford.edu</p>
|
||||
<p>650-724-0199</p>
|
||||
</section>
|
||||
|
||||
<section class="contact">
|
||||
<p>Lisa Lapin</p>
|
||||
<p>lapin@stanford.edu</p>
|
||||
<p>650-725-8396</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="footer">
|
||||
<hr class="horizontal-divider">
|
||||
<div class="logo"></div><h3 class="date">DATE: 04 - 03 - 2013</h3>
|
||||
<div class="social-sharing">
|
||||
<hr class="horizontal-divider">
|
||||
<p>Share with friends and family:</p>
|
||||
<a href="http://twitter.com/intent/tweet?text=:Stanford+to+work+with+edX+http://www.edx.org/press/stanford-to-work-with-edx" class="share">
|
||||
<img src="${static.url('images/social/twitter-sharing.png')}">
|
||||
</a>
|
||||
</a>
|
||||
<a href="mailto:?subject=Stanford%20to%20work%20with%20EdX…http://edx.org/press/stanford-to-work-with-edx" class="share">
|
||||
<img src="${static.url('images/social/email-sharing.png')}">
|
||||
</a>
|
||||
<div class="fb-like" data-href="http://edx.org/press/stanford-to-work-with-edx" data-send="true" data-width="450" data-show-faces="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
23
lms/templates/university_profile/utaustinx.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>UTAustinX</title></%block>
|
||||
|
||||
<%block name="university_header">
|
||||
<header class="search" style="background: url('/static/images/university/utaustin/utaustin-cover_2025x550.jpg')">
|
||||
<div class="inner-wrapper university-search">
|
||||
<hgroup>
|
||||
<div class="logo">
|
||||
<img src="${static.url('images/university/utaustin/utaustin-standalone_187x80.png')}" />
|
||||
</div>
|
||||
<h1>UTAustinx</h1>
|
||||
</hgroup>
|
||||
</div>
|
||||
</header>
|
||||
</%block>
|
||||
|
||||
<%block name="university_description">
|
||||
<p>The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools. UT Austin will be opening the Dell Medical School in 2016.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
@@ -1,5 +1,8 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%block name="title"><title>UTx</title></%block>
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
<%block name="university_description">
|
||||
<p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p>
|
||||
<p>Find out about <a href="${reverse('university_profile', args=['UTAustinX'])}">The University of Texas at Austin</a>.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
41
lms/urls.py
@@ -69,44 +69,22 @@ urlpatterns = ('',
|
||||
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'UTx'}),
|
||||
url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'WellesleyX'}),
|
||||
url(r'^university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'GeorgetownX'}),
|
||||
|
||||
# Dan accidentally sent out a press release with lower case urls for McGill, Toronto,
|
||||
# Rice, ANU, Delft, and EPFL. Hence the redirects.
|
||||
url(r'^university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'McGillX'}),
|
||||
url(r'^university_profile/mcgillx$',
|
||||
RedirectView.as_view(url='/university_profile/McGillX')),
|
||||
|
||||
url(r'^university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'TorontoX'}),
|
||||
url(r'^university_profile/torontox$',
|
||||
RedirectView.as_view(url='/university_profile/TorontoX')),
|
||||
|
||||
url(r'^university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'RiceX'}),
|
||||
url(r'^university_profile/ricex$',
|
||||
RedirectView.as_view(url='/university_profile/RiceX')),
|
||||
|
||||
url(r'^university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'ANUx'}),
|
||||
url(r'^university_profile/anux$',
|
||||
RedirectView.as_view(url='/university_profile/ANUx')),
|
||||
|
||||
url(r'^university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'DelftX'}),
|
||||
url(r'^university_profile/delftx$',
|
||||
RedirectView.as_view(url='/university_profile/DelftX')),
|
||||
|
||||
url(r'^university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'EPFLx'}),
|
||||
url(r'^university_profile/epflx$',
|
||||
RedirectView.as_view(url='/university_profile/EPFLx')),
|
||||
|
||||
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile',
|
||||
name="university_profile"),
|
||||
@@ -175,6 +153,9 @@ urlpatterns = ('',
|
||||
url(r'^press/xblock_announcement$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/xblock_announcement.html'},
|
||||
name="press/xblock-announcement"),
|
||||
url(r'^press/stanford-to-work-with-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/stanford_announcement.html'},
|
||||
name="press/stanford-to-work-with-edx"),
|
||||
|
||||
# Should this always update to point to the latest press release?
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to',
|
||||
|
||||
@@ -51,8 +51,9 @@ class LmsNamespace(Namespace):
|
||||
)
|
||||
|
||||
start = Date(help="Start time when this module is visible", scope=Scope.settings)
|
||||
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
|
||||
source_file = String(help="DO NOT USE", scope=Scope.settings)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
source_file = String(help="source file name (eg for latex)", scope=Scope.settings)
|
||||
giturl = String(help="url root for course data git repository", scope=Scope.settings)
|
||||
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
|
||||
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
|
||||
graceperiod = Timedelta(
|
||||
|
||||