Commit 6c67ed1f by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into feature/cdodge/course-overview-perf

Conflicts:
	common/lib/xmodule/xmodule/modulestore/mongo.py
parents 7a238935 a79a2907
...@@ -211,7 +211,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -211,7 +211,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
new_loc = descriptor.location._replace(org='MITx', course='999') new_loc = descriptor.location._replace(org='MITx', course='999')
print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_bad_contentstore_request(self):
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400)
def test_delete_course(self): def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -328,11 +332,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -328,11 +332,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(wrapper.counter, 4) self.assertEqual(wrapper.counter, 4)
# make sure we pre-fetched a known sequential which should be at depth=2 # make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential', self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data) 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
# make sure we don't have a specific vertical which should be at depth=3 # make sure we don't have a specific vertical which should be at depth=3
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
None]) in course.system.module_data) None]) in course.system.module_data)
def test_export_course_with_unknown_metadata(self): def test_export_course_with_unknown_metadata(self):
...@@ -556,7 +560,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -556,7 +560,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_children(parent.location, parent.children + [new_component_location.url()]) module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache # flush the cache
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level # check for grace period definition which should be defined at the course level
...@@ -571,7 +575,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -571,7 +575,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.update_metadata(new_module.location, own_metadata(new_module)) module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch # flush the cache and refetch
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
......
import datetime import datetime
import json import json
import copy import copy
from util import converters
from util.converters import jsdate_to_time
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
...@@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails, ...@@ -15,69 +13,13 @@ from models.settings.course_details import (CourseDetails,
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from django.test import TestCase
from .utils import ModuleStoreTestCase from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import time from xmodule.fields import Date
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase):
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_dates(self, date1, date2, expected_delta):
dt1 = ConvertersTestCase.struct_to_datetime(date1)
dt2 = ConvertersTestCase.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
+ str(date2) + "!=" + str(expected_delta))
def test_iso_to_struct(self):
'''Test conversion from iso compatible date strings to struct_time'''
self.compare_dates(converters.jsdate_to_time("2013-01-01"),
converters.jsdate_to_time("2012-12-31"),
datetime.timedelta(days=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00"),
converters.jsdate_to_time("2012-12-31T23"),
datetime.timedelta(hours=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"),
converters.jsdate_to_time("2012-12-31T23:59"),
datetime.timedelta(minutes=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"),
converters.jsdate_to_time("2012-12-31T23:59:59"),
datetime.timedelta(seconds=1))
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"),
converters.jsdate_to_time("2012-12-31T23:59:59Z"),
datetime.timedelta(seconds=1))
self.compare_dates(
converters.jsdate_to_time("2012-12-31T23:00:01-01:00"),
converters.jsdate_to_time("2013-01-01T00:00:00+01:00"),
datetime.timedelta(hours=1, seconds=1))
def test_struct_to_iso(self):
'''
Test converting time reprs to iso dates
'''
self.assertEqual(
converters.time_to_isodate(
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
"2012-12-31T23:59:59Z")
self.assertEqual(
converters.time_to_isodate(
jsdate_to_time("2012-12-31T23:59:59Z")),
"2012-12-31T23:59:59Z")
self.assertEqual(
converters.time_to_isodate(
jsdate_to_time("2012-12-31T23:00:01-01:00")),
"2013-01-01T00:00:01Z")
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
...@@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -206,17 +148,24 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_date_fields(self, details, encoded, context, field): def compare_date_fields(self, details, encoded, context, field):
if details[field] is not None: if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None: if field in encoded and encoded[field] is not None:
encoded_encoded = jsdate_to_time(encoded[field]) encoded_encoded = date.from_json(encoded[field])
dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded)
if isinstance(details[field], datetime.datetime): if isinstance(details[field], datetime.datetime):
dt2 = details[field] dt2 = details[field]
else: else:
details_encoded = jsdate_to_time(details[field]) details_encoded = date.from_json(details[field])
dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded)
expected_delta = datetime.timedelta(0) expected_delta = datetime.timedelta(0)
self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context)
......
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
...@@ -6,9 +5,9 @@ import json ...@@ -6,9 +5,9 @@ import json
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import time import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
from models.settings import course_grading from models.settings import course_grading
from contentstore.utils import update_item from contentstore.utils import update_item
from xmodule.fields import Date
import re import re
import logging import logging
...@@ -81,8 +80,14 @@ class CourseDetails(object): ...@@ -81,8 +80,14 @@ class CourseDetails(object):
dirty = False dirty = False
# In the descriptor's setter, the date is converted to JSON using Date's to_json method.
# Calling to_json on something that is already JSON doesn't work. Since reaching directly
# into the model is nasty, convert the JSON Date to a Python date, which is what the
# setter expects as input.
date = Date()
if 'start_date' in jsondict: if 'start_date' in jsondict:
converted = jsdate_to_time(jsondict['start_date']) converted = date.from_json(jsondict['start_date'])
else: else:
converted = None converted = None
if converted != descriptor.start: if converted != descriptor.start:
...@@ -90,7 +95,7 @@ class CourseDetails(object): ...@@ -90,7 +95,7 @@ class CourseDetails(object):
descriptor.start = converted descriptor.start = converted
if 'end_date' in jsondict: if 'end_date' in jsondict:
converted = jsdate_to_time(jsondict['end_date']) converted = date.from_json(jsondict['end_date'])
else: else:
converted = None converted = None
...@@ -99,7 +104,7 @@ class CourseDetails(object): ...@@ -99,7 +104,7 @@ class CourseDetails(object):
descriptor.end = converted descriptor.end = converted
if 'enrollment_start' in jsondict: if 'enrollment_start' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_start']) converted = date.from_json(jsondict['enrollment_start'])
else: else:
converted = None converted = None
...@@ -108,7 +113,7 @@ class CourseDetails(object): ...@@ -108,7 +113,7 @@ class CourseDetails(object):
descriptor.enrollment_start = converted descriptor.enrollment_start = converted
if 'enrollment_end' in jsondict: if 'enrollment_end' in jsondict:
converted = jsdate_to_time(jsondict['enrollment_end']) converted = date.from_json(jsondict['enrollment_end'])
else: else:
converted = None converted = None
...@@ -178,6 +183,6 @@ class CourseSettingsEncoder(json.JSONEncoder): ...@@ -178,6 +183,6 @@ class CourseSettingsEncoder(json.JSONEncoder):
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
elif isinstance(obj, time.struct_time): elif isinstance(obj, time.struct_time):
return time_to_date(obj) return Date().to_json(obj)
else: else:
return JSONEncoder.default(self, obj) return JSONEncoder.default(self, obj)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
import re
from util import converters
from datetime import timedelta from datetime import timedelta
......
...@@ -81,7 +81,7 @@ $(document).ready(function () { ...@@ -81,7 +81,7 @@ $(document).ready(function () {
}); });
// general link management - smooth scrolling page links // general link management - smooth scrolling page links
$('a[rel*="view"]').bind('click', linkSmoothScroll); $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// toggling overview section details // toggling overview section details
...@@ -148,7 +148,7 @@ $(document).ready(function () { ...@@ -148,7 +148,7 @@ $(document).ready(function () {
}); });
}); });
function linkSmoothScroll(e) { function smoothScrollLink(e) {
(e).preventDefault(); (e).preventDefault();
$.smoothScroll({ $.smoothScroll({
......
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
<figcaption class="description">Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.</figcaption> <figcaption class="description">Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.</figcaption>
</figure> </figure>
<a href="#" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
<figcaption class="description">Quickly create videos, text snippets, inline discussions, and a variety of problem types.</figcaption> <figcaption class="description">Quickly create videos, text snippets, inline discussions, and a variety of problem types.</figcaption>
</figure> </figure>
<a href="#" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
...@@ -177,7 +177,7 @@ ...@@ -177,7 +177,7 @@
<figcaption class="description">Simply set the date of a section or subsection, and Studio will publish it to your students for you.</figcaption> <figcaption class="description">Simply set the date of a section or subsection, and Studio will publish it to your students for you.</figcaption>
</figure> </figure>
<a href="#" rel="view" class="action action-modal-close"> <a href="" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i> <i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span> <span class="label">close modal</span>
</a> </a>
......
...@@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified ...@@ -5,6 +5,7 @@ from django.http import HttpResponse, Http404, HttpResponseNotModified
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
from xmodule.modulestore import InvalidLocationError
from cache_toolbox.core import get_cached_content, set_cached_content from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -13,7 +14,14 @@ class StaticContentServer(object): ...@@ -13,7 +14,14 @@ class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag # look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'):
loc = StaticContent.get_location_from_path(request.path) try:
loc = StaticContent.get_location_from_path(request.path)
except InvalidLocationError:
# return a 'Bad Request' to browser as we have a malformed Location
response = HttpResponse()
response.status_code = 400
return response
# first look in our cache so we don't have to round-trip to the DB # first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(loc) content = get_cached_content(loc)
if content is None: if content is None:
......
...@@ -325,7 +325,12 @@ def change_enrollment(request): ...@@ -325,7 +325,12 @@ def change_enrollment(request):
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) try:
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
except IntegrityError:
# If we've already created this enrollment in a separate transaction,
# then just continue
pass
return {'success': True} return {'success': True}
elif action == "unenroll": elif action == "unenroll":
...@@ -369,14 +374,14 @@ def login_user(request, error=""): ...@@ -369,14 +374,14 @@ def login_user(request, error=""):
try: try:
user = User.objects.get(email=email) user = User.objects.get(email=email)
except User.DoesNotExist: except User.DoesNotExist:
log.warning("Login failed - Unknown user email: {0}".format(email)) log.warning(u"Login failed - Unknown user email: {0}".format(email))
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) # TODO: User error message 'value': 'Email or password is incorrect.'})) # TODO: User error message
username = user.username username = user.username
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user is None: if user is None:
log.warning("Login failed - password for {0} is invalid".format(email)) log.warning(u"Login failed - password for {0} is invalid".format(email))
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': 'Email or password is incorrect.'})) 'value': 'Email or password is incorrect.'}))
...@@ -392,7 +397,7 @@ def login_user(request, error=""): ...@@ -392,7 +397,7 @@ def login_user(request, error=""):
log.critical("Login failed - Could not create session. Is memcached running?") log.critical("Login failed - Could not create session. Is memcached running?")
log.exception(e) log.exception(e)
log.info("Login success - {0} ({1})".format(username, email)) log.info(u"Login success - {0} ({1})".format(username, email))
try_change_enrollment(request) try_change_enrollment(request)
...@@ -400,7 +405,7 @@ def login_user(request, error=""): ...@@ -400,7 +405,7 @@ def login_user(request, error=""):
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user) reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \ not_activated_msg = "This account has not been activated. We have " + \
......
import time
import datetime
import calendar
import dateutil.parser
def time_to_date(time_obj):
"""
Convert a time.time_struct to a true universal time (can pass to js Date
constructor)
"""
return calendar.timegm(time_obj) * 1000
def time_to_isodate(source):
'''Convert to an iso date'''
if isinstance(source, time.struct_time):
return time.strftime('%Y-%m-%dT%H:%M:%SZ', source)
elif isinstance(source, datetime):
return source.isoformat() + 'Z'
def jsdate_to_time(field):
"""
Convert a universal time (iso format) or msec since epoch to a time obj
"""
if field is None:
return field
elif isinstance(field, basestring):
d = dateutil.parser.parse(field)
return d.utctimetuple()
elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
return field
else:
raise ValueError("Couldn't convert %r to time" % field)
...@@ -1961,9 +1961,10 @@ class ImageResponse(LoncapaResponse): ...@@ -1961,9 +1961,10 @@ class ImageResponse(LoncapaResponse):
self.ielements = self.inputfields self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements] self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers): def get_score(self, student_answers):
correct_map = CorrectMap() correct_map = CorrectMap()
expectedset = self.get_answers() expectedset = self.get_mapped_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> for aid in self.answer_ids: # loop through IDs of <imageinput>
# fields in our stanza # fields in our stanza
given = student_answers[ given = student_answers[
...@@ -2018,11 +2019,42 @@ class ImageResponse(LoncapaResponse): ...@@ -2018,11 +2019,42 @@ class ImageResponse(LoncapaResponse):
break break
return correct_map return correct_map
def get_answers(self): def get_mapped_answers(self):
return ( '''
Returns the internal representation of the answers
Input:
None
Returns:
tuple (dict, dict) -
rectangles (dict) - a map of inputs to the defined rectangle for that input
regions (dict) - a map of inputs to the defined region for that input
'''
answers = (
dict([(ie.get('id'), ie.get( dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]), 'rectangle')) for ie in self.ielements]),
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
return answers
def get_answers(self):
'''
Returns the external representation of the answers
Input:
None
Returns:
dict (str, (str, str)) - a map of inputs to a tuple of their rectange
and their regions
'''
answers = {}
for ie in self.ielements:
ie_id = ie.get('id')
answers[ie_id] = (ie.get('rectangle'), ie.get('regions'))
return answers
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -2087,8 +2119,8 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2087,8 +2119,8 @@ class AnnotationResponse(LoncapaResponse):
correct_option = self._find_option_with_choice( correct_option = self._find_option_with_choice(
inputfield, 'correct') inputfield, 'correct')
if correct_option is not None: if correct_option is not None:
answer_map[inputfield.get( input_id = inputfield.get('id')
'id')] = correct_option.get('description') answer_map[input_id] = correct_option.get('description')
return answer_map return answer_map
def _get_max_points(self): def _get_max_points(self):
......
...@@ -36,6 +36,10 @@ class ResponseTest(unittest.TestCase): ...@@ -36,6 +36,10 @@ class ResponseTest(unittest.TestCase):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness) self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness)
def assert_answer_format(self, problem):
answers = problem.get_question_answers()
self.assertTrue(answers['1_2_1'] is not None)
def assert_multiple_grade(self, problem, correct_answers, incorrect_answers): def assert_multiple_grade(self, problem, correct_answers, incorrect_answers):
for input_str in correct_answers: for input_str in correct_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
...@@ -166,6 +170,13 @@ class ImageResponseTest(ResponseTest): ...@@ -166,6 +170,13 @@ class ImageResponseTest(ResponseTest):
incorrect_inputs = ["[0,0]", "[600,300]"] incorrect_inputs = ["[0,0]", "[600,300]"]
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
def test_show_answer(self):
rectangle_str = "(100,100)-(200,200)"
region_str = "[[10,10], [20,10], [20, 30]]"
problem = self.build_problem(regions=region_str, rectangle=rectangle_str)
self.assert_answer_format(problem)
class SymbolicResponseTest(unittest.TestCase): class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self): def test_sr_grade(self):
......
...@@ -7,6 +7,8 @@ import requests ...@@ -7,6 +7,8 @@ import requests
import time import time
from datetime import datetime from datetime import datetime
import dateutil.parser
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
...@@ -150,7 +152,7 @@ class CourseFields(object): ...@@ -150,7 +152,7 @@ class CourseFields(object):
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings) start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = StringOrDate(help="Date that this course is advertised to start", scope=Scope.settings) advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content) grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -537,10 +539,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
announcement = self.announcement announcement = self.announcement
if announcement is not None: if announcement is not None:
announcement = to_datetime(announcement) announcement = to_datetime(announcement)
if self.advertised_start is None or isinstance(self.advertised_start, basestring):
try:
start = dateutil.parser.parse(self.advertised_start)
except (ValueError, AttributeError):
start = to_datetime(self.start) start = to_datetime(self.start)
else:
start = to_datetime(self.advertised_start)
now = to_datetime(time.gmtime()) now = to_datetime(time.gmtime())
return announcement, start, now return announcement, start, now
...@@ -631,8 +635,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -631,8 +635,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def start_date_text(self): def start_date_text(self):
def try_parse_iso_8601(text):
try:
result = datetime.strptime(text, "%Y-%m-%dT%H:%M")
result = result.strftime("%b %d, %Y")
except ValueError:
result = text.title()
return result
if isinstance(self.advertised_start, basestring): if isinstance(self.advertised_start, basestring):
return self.advertised_start return try_parse_iso_8601(self.advertised_start)
elif self.advertised_start is None and self.start is None: elif self.advertised_start is None and self.start is None:
return 'TBD' return 'TBD'
else: else:
......
...@@ -14,7 +14,6 @@ class Date(ModelType): ...@@ -14,7 +14,6 @@ class Date(ModelType):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Date fields know how to parse and produce json (iso) compatible formats.
''' '''
# NB: these are copies of util.converters.*
def from_json(self, field): def from_json(self, field):
""" """
Parse an optional metadata key containing a time: if present, complain Parse an optional metadata key containing a time: if present, complain
...@@ -23,6 +22,8 @@ class Date(ModelType): ...@@ -23,6 +22,8 @@ class Date(ModelType):
""" """
if field is None: if field is None:
return field return field
elif field is "":
return None
elif isinstance(field, basestring): elif isinstance(field, basestring):
d = dateutil.parser.parse(field) d = dateutil.parser.parse(field)
return d.utctimetuple() return d.utctimetuple()
......
import pymongo import pymongo
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup from mock import Mock
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup, assert_false
from pprint import pprint from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -102,3 +103,58 @@ class TestMongoModuleStore(object): ...@@ -102,3 +103,58 @@ class TestMongoModuleStore(object):
def test_path_to_location(self): def test_path_to_location(self):
'''Make sure that path_to_location works''' '''Make sure that path_to_location works'''
check_path_to_location(self.store) check_path_to_location(self.store)
def test_metadata_inheritance_query_count(self):
'''
When retrieving items from mongo, we should only query the cache a number of times
equal to the number of courses being retrieved from.
We should also not query
'''
self.store.metadata_inheritance_cache = Mock()
get_many = self.store.metadata_inheritance_cache.get_many
set_many = self.store.metadata_inheritance_cache.set_many
get_many.return_value = {('edX', 'toy'): {}}
self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=0)
assert_false(get_many.called)
assert_false(set_many.called)
get_many.reset_mock()
self.store.get_item(Location("i4x://edX/toy/course/2012_Fall"), depth=3)
get_many.assert_called_with([('edX', 'toy')])
assert_equals(0, set_many.call_count)
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=0)
assert_false(get_many.called)
assert_false(set_many.called)
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, 'course', None), depth=3)
assert_equals(1, get_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0]))
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys()))
get_many.reset_mock()
self.store.get_items(Location('i4x', 'edX', None, None, None), depth=0)
assert_equals(1, get_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(get_many.call_args[0][0]))
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple')], sorted(set_many.call_args[0][0].keys()))
get_many.reset_mock()
def test_metadata_inheritance_query_count_forced_refresh(self):
self.store.metadata_inheritance_cache = Mock()
get_many = self.store.metadata_inheritance_cache.get_many
set_many = self.store.metadata_inheritance_cache.set_many
get_many.return_value = {('edX', 'toy'): {}}
self.store.get_cached_metadata_inheritance_trees(
[Location("i4x://edX/toy/course/2012_Fall"), Location("i4x://edX/simple/course/2012_Fall")],
True
)
assert_false(get_many.called)
assert_equals(1, set_many.call_count)
assert_equals([('edX', 'simple'), ('edX', 'toy')], sorted(set_many.call_args[0][0].keys()))
...@@ -24,7 +24,7 @@ MAX_ATTEMPTS = 1 ...@@ -24,7 +24,7 @@ MAX_ATTEMPTS = 1
MAX_SCORE = 1 MAX_SCORE = 1
#The highest score allowed for the overall xmodule and for each rubric point #The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 3 MAX_SCORE_ALLOWED = 50
#If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress #If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
#Metadata overrides this. #Metadata overrides this.
...@@ -363,7 +363,15 @@ class CombinedOpenEndedV1Module(): ...@@ -363,7 +363,15 @@ class CombinedOpenEndedV1Module():
""" """
self.update_task_states() self.update_task_states()
html = self.current_task.get_html(self.system) html = self.current_task.get_html(self.system)
return_html = rewrite_links(html, self.rewrite_content_links) return_html = html
try:
#Without try except block, get this error:
# File "/home/vik/mitx_all/mitx/common/lib/xmodule/xmodule/x_module.py", line 263, in rewrite_content_links
# if link.startswith(XASSET_SRCREF_PREFIX):
# Placing try except so that if the error is fixed, this code will start working again.
return_html = rewrite_links(html, self.rewrite_content_links)
except:
pass
return return_html return return_html
def get_current_attributes(self, task_number): def get_current_attributes(self, task_number):
...@@ -782,7 +790,7 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -782,7 +790,7 @@ class CombinedOpenEndedV1Descriptor():
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
def __init__(self, system): def __init__(self, system):
self.system =system self.system = system
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
......
...@@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [ ...@@ -36,7 +36,7 @@ ALLOWABLE_IMAGE_SUFFIXES = [
] ]
#Maximum allowed dimensions (x and y) for an uploaded image #Maximum allowed dimensions (x and y) for an uploaded image
MAX_ALLOWED_IMAGE_DIM = 1500 MAX_ALLOWED_IMAGE_DIM = 2000
#Dimensions to which image is resized before it is evaluated for color count, etc #Dimensions to which image is resized before it is evaluated for color count, etc
MAX_IMAGE_DIM = 150 MAX_IMAGE_DIM = 150
...@@ -178,7 +178,7 @@ class URLProperties(object): ...@@ -178,7 +178,7 @@ class URLProperties(object):
Runs all available url tests Runs all available url tests
@return: True if URL passes tests, false if not. @return: True if URL passes tests, false if not.
""" """
url_is_okay = self.check_suffix() and self.check_if_parses() and self.check_domain() url_is_okay = self.check_suffix() and self.check_if_parses()
return url_is_okay return url_is_okay
def check_domain(self): def check_domain(self):
......
...@@ -357,10 +357,6 @@ class OpenEndedChild(object): ...@@ -357,10 +357,6 @@ class OpenEndedChild(object):
if get_data['can_upload_files'] in ['true', '1']: if get_data['can_upload_files'] in ['true', '1']:
has_file_to_upload = True has_file_to_upload = True
file = get_data['student_file'][0] file = get_data['student_file'][0]
if self.system.track_fuction:
self.system.track_function('open_ended_image_upload', {'filename': file.name})
else:
log.info("No tracking function found when uploading image.")
uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file) uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file)
if uploaded_to_s3: if uploaded_to_s3:
image_tag = self.generate_image_tag_from_url(s3_public_url, file.name) image_tag = self.generate_image_tag_from_url(s3_public_url, file.name)
......
import unittest import unittest
from time import strptime from time import strptime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
...@@ -89,25 +90,41 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -89,25 +90,41 @@ class IsNewCourseTestCase(unittest.TestCase):
((day2, None, None), (day1, None, None), self.assertLess), ((day2, None, None), (day1, None, None), self.assertLess),
((day1, None, None), (day1, None, None), self.assertEqual), ((day1, None, None), (day1, None, None), self.assertEqual),
# Non-parseable advertised starts are ignored in preference # Non-parseable advertised starts are ignored in preference to actual starts
# to actual starts ((day2, None, "Spring"), (day1, None, "Fall"), self.assertLess),
((day2, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertLess), ((day1, None, "Spring"), (day1, None, "Fall"), self.assertEqual),
((day1, None, "Spring 2013"), (day1, None, "Fall 2012"), self.assertEqual),
# Partially parsable advertised starts should take priority over start dates
((day2, None, "October 2013"), (day2, None, "October 2012"), self.assertLess),
((day2, None, "October 2013"), (day1, None, "October 2013"), self.assertEqual),
# Parseable advertised starts take priority over start dates # Parseable advertised starts take priority over start dates
((day1, None, day2), (day1, None, day1), self.assertLess), ((day1, None, day2), (day1, None, day1), self.assertLess),
((day2, None, day2), (day1, None, day2), self.assertEqual), ((day2, None, day2), (day1, None, day2), self.assertEqual),
] ]
data = []
for a, b, assertion in dates: for a, b, assertion in dates:
a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score
b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score
print "Comparing %s to %s" % (a, b) print "Comparing %s to %s" % (a, b)
assertion(a_score, b_score) assertion(a_score, b_score)
@patch('xmodule.course_module.time.gmtime')
def test_start_date_text(self, gmtime_mock):
gmtime_mock.return_value = NOW
settings = [
# start, advertized, result
('2012-12-02T12:00', None, 'Dec 02, 2012'),
('2012-12-02T12:00', '2011-11-01T12:00', 'Nov 01, 2011'),
('2012-12-02T12:00', 'Spring 2012', 'Spring 2012'),
('2012-12-02T12:00', 'November, 2011', 'November, 2011'),
]
for s in settings:
d = self.get_dummy_course(start=s[0], advertised_start=s[1])
print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2])
@patch('xmodule.course_module.time.gmtime') @patch('xmodule.course_module.time.gmtime')
def test_is_newish(self, gmtime_mock): def test_is_newish(self, gmtime_mock):
......
"""Tests for Date class defined in fields.py."""
import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date
import time
class DateTest(unittest.TestCase):
date = Date()
@staticmethod
def struct_to_datetime(struct_time):
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
struct_time.tm_mday, struct_time.tm_hour,
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
def compare_dates(self, date1, date2, expected_delta):
dt1 = DateTest.struct_to_datetime(date1)
dt2 = DateTest.struct_to_datetime(date2)
self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-"
+ str(date2) + "!=" + str(expected_delta))
def test_from_json(self):
'''Test conversion from iso compatible date strings to struct_time'''
self.compare_dates(
DateTest.date.from_json("2013-01-01"),
DateTest.date.from_json("2012-12-31"),
datetime.timedelta(days=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00"),
DateTest.date.from_json("2012-12-31T23"),
datetime.timedelta(hours=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00"),
DateTest.date.from_json("2012-12-31T23:59"),
datetime.timedelta(minutes=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00:00"),
DateTest.date.from_json("2012-12-31T23:59:59"),
datetime.timedelta(seconds=1))
self.compare_dates(
DateTest.date.from_json("2013-01-01T00:00:00Z"),
DateTest.date.from_json("2012-12-31T23:59:59Z"),
datetime.timedelta(seconds=1))
self.compare_dates(
DateTest.date.from_json("2012-12-31T23:00:01-01:00"),
DateTest.date.from_json("2013-01-01T00:00:00+01:00"),
datetime.timedelta(hours=1, seconds=1))
def test_return_None(self):
self.assertIsNone(DateTest.date.from_json(""))
self.assertIsNone(DateTest.date.from_json(None))
self.assertIsNone(DateTest.date.from_json(['unknown value']))
def test_old_due_date_format(self):
current = datetime.datetime.today()
self.assertEqual(
time.struct_time((current.year, 3, 12, 12, 0, 0, 1, 71, 0)),
DateTest.date.from_json("March 12 12:00"))
self.assertEqual(
time.struct_time((current.year, 12, 4, 16, 30, 0, 2, 338, 0)),
DateTest.date.from_json("December 4 16:30"))
def test_to_json(self):
'''
Test converting time reprs to iso dates
'''
self.assertEqual(
DateTest.date.to_json(
time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")),
"2012-12-31T23:59:59Z")
self.assertEqual(
DateTest.date.to_json(
DateTest.date.from_json("2012-12-31T23:59:59Z")),
"2012-12-31T23:59:59Z")
self.assertEqual(
DateTest.date.to_json(
DateTest.date.from_json("2012-12-31T23:00:01-01:00")),
"2013-01-01T00:00:01Z")
/* This file defines a processor in between the student's math input
(AsciiMath) and what is read by MathJax. It allows for our own
customizations, such as use of the syntax "a_b__x" in superscripts, or
possibly coloring certain variables, etc&.
It is used in the <textline> definition like the following:
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
*/
window.SymbolicMathjaxPreprocessor = function () {
this.fn = function (eqn) {
// flags and config
var superscriptsOn = true;
if (superscriptsOn) {
// find instances of "__" and make them superscripts ("^") and tag them
// as such. Specifcally replace instances of "__X" or "__{XYZ}" with
// "^{CHAR$1}", marking superscripts as different from powers
// a zero width space--this is an invisible character that no one would
// use, that gets passed through MathJax and to the server
var c = "\u200b";
eqn = eqn.replace(/__(?:([^\{])|\{([^\}]+)\})/g, '^{' + c + '$1$2}');
// NOTE: MathJax supports '\class{name}{mathcode}' but not for asciimath
// input, which is too bad. This would be preferable to this char tag
}
return eqn;
};
};
#################
Symbolic Response
#################
This document plans to document features that the current symbolic response
supports. In general it allows the input and validation of math expressions,
up to commutativity and some identities.
********
Features
********
This is a partial list of features, to be revised as we go along:
* sub and superscripts: an expression following the ``^`` character
indicates exponentiation. To use superscripts in variables, the syntax
is ``b_x__d`` for the variable ``b`` with subscript ``x`` and super
``d``.
An example of a problem::
<symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse>
It's a bit of a pain to enter that.
* The script-style math variant. What would be outputted in latex if you
entered ``\mathcal{N}``. This is used in some variables.
An example::
<symbolicresponse expect="scriptN_B + x" size="30">
<textline math="1"/>
</symbolicresponse>
There is no fancy preprocessing needed, but if you had superscripts or
something, you would need to include that part.
...@@ -3,13 +3,11 @@ from django.test.utils import override_settings ...@@ -3,13 +3,11 @@ from django.test.utils import override_settings
import xmodule.modulestore.django 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.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class WikiRedirectTestCase(PageLoader): class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self): def setUp(self):
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses() courses = modulestore().get_courses()
...@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -30,8 +28,6 @@ class WikiRedirectTestCase(PageLoader):
self.activate_user(self.student) self.activate_user(self.student)
self.activate_user(self.instructor) self.activate_user(self.instructor)
def test_wiki_redirect(self): def test_wiki_redirect(self):
""" """
Test that requesting wiki URLs redirect properly to or out of classes. Test that requesting wiki URLs redirect properly to or out of classes.
...@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -69,7 +65,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
self.assertEqual(resp['Location'], 'http://testserver' + destination) self.assertEqual(resp['Location'], 'http://testserver' + destination)
def create_course_page(self, course): def create_course_page(self, course):
""" """
Test that loading the course wiki page creates the wiki page. Test that loading the course wiki page creates the wiki page.
...@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -98,7 +93,6 @@ class WikiRedirectTestCase(PageLoader):
self.assertTrue("course info" in resp.content.lower()) self.assertTrue("course info" in resp.content.lower())
self.assertTrue("courseware" in resp.content.lower()) self.assertTrue("courseware" in resp.content.lower())
def test_course_navigator(self): def test_course_navigator(self):
"""" """"
Test that going from a course page to a wiki page contains the course navigator. Test that going from a course page to a wiki page contains the course navigator.
...@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader): ...@@ -108,7 +102,6 @@ class WikiRedirectTestCase(PageLoader):
self.enroll(self.toy) self.enroll(self.toy)
self.create_course_page(self.toy) self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={'course_id': self.toy.id}) referer = reverse("courseware", kwargs={'course_id': self.toy.id})
......
...@@ -8,6 +8,7 @@ from functools import partial ...@@ -8,6 +8,7 @@ from functools import partial
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
...@@ -208,9 +209,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -208,9 +209,6 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS '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 #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 an S3 interface to upload images to S3
#It needs the open ended grading interface in order to get peer grading to be done #It needs the open ended grading interface in order to get peer grading to be done
...@@ -226,12 +224,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -226,12 +224,11 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING
if is_descriptor_combined_open_ended: if is_descriptor_combined_open_ended:
s3_interface = { s3_interface = {
'access_key' : get_or_default('AWS_ACCESS_KEY_ID',''), 'access_key' : getattr(settings,'AWS_ACCESS_KEY_ID',''),
'secret_access_key' : get_or_default('AWS_SECRET_ACCESS_KEY',''), 'secret_access_key' : getattr(settings,'AWS_SECRET_ACCESS_KEY',''),
'storage_bucket_name' : get_or_default('AWS_STORAGE_BUCKET_NAME','') 'storage_bucket_name' : getattr(settings,'AWS_STORAGE_BUCKET_NAME','openended')
} }
def inner_get_module(descriptor): def inner_get_module(descriptor):
""" """
Delegate to get_module. It does an access check, so may return None Delegate to get_module. It does an access check, so may return None
...@@ -412,6 +409,9 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -412,6 +409,9 @@ def modx_dispatch(request, dispatch, location, course_id):
if not Location.is_valid(location): if not Location.is_valid(location):
raise Http404("Invalid location") raise Http404("Invalid location")
if not request.user.is_authenticated():
raise PermissionDenied
# Check for submitted files and basic file size checks # Check for submitted files and basic file size checks
p = request.POST.copy() p = request.POST.copy()
if request.FILES: if request.FILES:
......
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)
import logging from mock import MagicMock
from mock import MagicMock, patch
import json import json
import factory
import unittest
from nose.tools import set_trace
from django.http import Http404, HttpResponse, HttpRequest from django.http import Http404, HttpResponse
from django.conf import settings from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.test.client import Client
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.exceptions import NotFoundError from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
import courseware.module_render as render import courseware.module_render as render
from xmodule.modulestore.django import modulestore, _MODULESTORES from courseware.tests.tests import LoginEnrollmentTestCase
from xmodule.seq_module import SequenceModule
from courseware.tests.tests import PageLoader
from student.models import Registration
from courseware.model_data import ModelDataCache from courseware.model_data import ModelDataCache
from .factories import UserFactory from .factories import UserFactory
...@@ -49,10 +38,9 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) ...@@ -49,10 +38,9 @@ TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class ModuleRenderTestCase(PageLoader): class ModuleRenderTestCase(LoginEnrollmentTestCase):
def setUp(self): def setUp(self):
self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview'] self.location = ['i4x', 'edX', 'toy', 'chapter', 'Overview']
self._MODULESTORES = {}
self.course_id = 'edX/toy/2012_Fall' self.course_id = 'edX/toy/2012_Fall'
self.toy_course = modulestore().get_course(self.course_id) self.toy_course = modulestore().get_course(self.course_id)
...@@ -66,10 +54,9 @@ class ModuleRenderTestCase(PageLoader): ...@@ -66,10 +54,9 @@ class ModuleRenderTestCase(PageLoader):
mock_request = MagicMock() mock_request = MagicMock()
mock_request.FILES.keys.return_value = ['file_id'] mock_request.FILES.keys.return_value = ['file_id']
mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1) mock_request.FILES.getlist.return_value = ['file'] * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)
self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, self.assertEquals(render.modx_dispatch(mock_request, 'dummy', self.location, 'dummy').content,
'dummy').content, json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' %
json.dumps({'success': 'Submission aborted! Maximum %d files may be submitted at once' % settings.MAX_FILEUPLOADS_PER_INPUT}))
settings.MAX_FILEUPLOADS_PER_INPUT}))
mock_request_2 = MagicMock() mock_request_2 = MagicMock()
mock_request_2.FILES.keys.return_value = ['file_id'] mock_request_2.FILES.keys.return_value = ['file_id']
inputfile = Stub() inputfile = Stub()
...@@ -80,7 +67,7 @@ class ModuleRenderTestCase(PageLoader): ...@@ -80,7 +67,7 @@ class ModuleRenderTestCase(PageLoader):
self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location, self.assertEquals(render.modx_dispatch(mock_request_2, 'dummy', self.location,
'dummy').content, 'dummy').content,
json.dumps({'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % 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 = MagicMock()
mock_request_3.POST.copy.return_value = {} mock_request_3.POST.copy.return_value = {}
mock_request_3.FILES = False mock_request_3.FILES = False
...@@ -91,10 +78,10 @@ class ModuleRenderTestCase(PageLoader): ...@@ -91,10 +78,10 @@ class ModuleRenderTestCase(PageLoader):
self.assertRaises(ItemNotFoundError, render.modx_dispatch, self.assertRaises(ItemNotFoundError, render.modx_dispatch,
mock_request_3, 'dummy', self.location, 'toy') mock_request_3, 'dummy', self.location, 'toy')
self.assertRaises(Http404, render.modx_dispatch, mock_request_3, 'dummy', 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} mock_request_3.POST.copy.return_value = {'position': 1}
self.assertIsInstance(render.modx_dispatch(mock_request_3, 'goto_position', 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): def test_get_score_bucket(self):
self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(0, 10), 'incorrect')
...@@ -104,12 +91,23 @@ class ModuleRenderTestCase(PageLoader): ...@@ -104,12 +91,23 @@ class ModuleRenderTestCase(PageLoader):
self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(11, 10), 'incorrect')
self.assertEquals(render.get_score_bucket(-1, 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) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestTOC(TestCase): class TestTOC(TestCase):
"""Check the Table of Contents for a course""" """Check the Table of Contents for a course"""
def setUp(self): def setUp(self):
self._MODULESTORES = {}
# Toy courses should be loaded # Toy courses should be loaded
self.course_name = 'edX/toy/2012_Fall' self.course_name = 'edX/toy/2012_Fall'
...@@ -125,19 +123,19 @@ class TestTOC(TestCase): ...@@ -125,19 +123,19 @@ class TestTOC(TestCase):
self.toy_course.id, self.portal_user, self.toy_course, depth=2) self.toy_course.id, self.portal_user, self.toy_course, depth=2)
expected = ([{'active': True, 'sections': expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False}, 'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': '', 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) '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) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
...@@ -152,19 +150,19 @@ class TestTOC(TestCase): ...@@ -152,19 +150,19 @@ class TestTOC(TestCase):
self.toy_course.id, self.portal_user, self.toy_course, depth=2) self.toy_course.id, self.portal_user, self.toy_course, depth=2)
expected = ([{'active': True, 'sections': expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False}, 'format': u'Lecture Sequence', 'due': '', 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': True}, 'format': '', 'due': '', 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': '', 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': '', 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': '', 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) '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) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
import json import json
import logging import logging
import xml.sax.saxutils as saxutils
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST from django.http import Http404
from django.http import HttpResponse, Http404
from django.utils import simplejson
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access 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 courseware.access import has_access
from urllib import urlencode from django_comment_client.permissions import cached_has_permission
from operator import methodcaller from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
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)
import django_comment_client.utils as utils import django_comment_client.utils as utils
import comment_client as cc import comment_client as cc
import xml.sax.saxutils as saxutils
THREADS_PER_PAGE = 20 THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20
...@@ -31,6 +25,7 @@ escapedict = {'"': '&quot;'} ...@@ -31,6 +25,7 @@ escapedict = {'"': '&quot;'}
log = logging.getLogger("edx.discussions") log = logging.getLogger("edx.discussions")
@login_required
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE): def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
""" """
This may raise cc.utils.CommentClientError or 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 ...@@ -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.default_sort_key = request.GET.get('sort_key')
cc_user.save() cc_user.save()
#there are 2 dimensions to consider when executing a search with respect to group id #there are 2 dimensions to consider when executing a search with respect to group id
#is user a moderator #is user a moderator
#did the user request a group #did the user request a group
...@@ -91,18 +85,17 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -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 #now add the group name if the thread has a group id
for thread in threads: for thread in threads:
if thread.get('group_id'): if thread.get('group_id'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name 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']) thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
else: else:
thread['group_name'] = "" thread['group_name'] = ""
thread['group_string'] = "This post visible to everyone." thread['group_string'] = "This post visible to everyone."
#patch for backward compatibility to comments service #patch for backward compatibility to comments service
if not 'pinned' in thread: if not 'pinned' in thread:
thread['pinned'] = False thread['pinned'] = False
query_params['page'] = page query_params['page'] = page
query_params['num_pages'] = num_pages query_params['num_pages'] = num_pages
...@@ -110,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -110,6 +103,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
return threads, query_params return threads, query_params
@login_required
def inline_discussion(request, course_id, discussion_id): def inline_discussion(request, course_id, discussion_id):
""" """
Renders JSON for DiscussionModules Renders JSON for DiscussionModules
...@@ -142,14 +136,14 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -142,14 +136,14 @@ def inline_discussion(request, course_id, discussion_id):
cohorts_list = list() cohorts_list = list()
if is_cohorted: 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 you're a mod, send all cohorts and let you pick
if is_moderator: if is_moderator:
cohorts = get_course_cohorts(course_id) cohorts = get_course_cohorts(course_id)
for c in cohorts: for c in cohorts:
cohorts_list.append({'name':c.name, 'id':c.id}) cohorts_list.append({'name': c.name, 'id': c.id})
else: else:
#students don't get to choose #students don't get to choose
...@@ -216,9 +210,6 @@ def forum_form_discussion(request, course_id): ...@@ -216,9 +210,6 @@ def forum_form_discussion(request, course_id):
user_cohort_id = get_cohort_id(request.user, course_id) user_cohort_id = get_cohort_id(request.user, course_id)
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
...@@ -242,6 +233,7 @@ def forum_form_discussion(request, course_id): ...@@ -242,6 +233,7 @@ def forum_form_discussion(request, course_id):
return render_to_response('discussion/index.html', context) return render_to_response('discussion/index.html', context)
@login_required @login_required
def single_thread(request, course_id, discussion_id, thread_id): def single_thread(request, course_id, discussion_id, thread_id):
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
...@@ -250,11 +242,11 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -250,11 +242,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try: try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
#patch for backward compatibility with comments service #patch for backward compatibility with comments service
if not 'pinned' in thread.attributes: if not 'pinned' in thread.attributes:
thread['pinned'] = False thread['pinned'] = False
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.") log.error("Error loading single thread.")
raise Http404 raise Http404
...@@ -352,7 +344,7 @@ def user_profile(request, course_id, user_id): ...@@ -352,7 +344,7 @@ def user_profile(request, course_id, user_id):
query_params = { query_params = {
'page': request.GET.get('page', 1), 'page': request.GET.get('page', 1),
'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities 'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities
} }
threads, page, num_pages = profiled_user.active_threads(query_params) threads, page, num_pages = profiled_user.active_threads(query_params)
query_params['page'] = page query_params['page'] = page
...@@ -369,8 +361,6 @@ def user_profile(request, course_id, user_id): ...@@ -369,8 +361,6 @@ def user_profile(request, course_id, user_id):
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
}) })
else: else:
context = { context = {
'course': course, 'course': course,
'user': request.user, 'user': request.user,
...@@ -426,5 +416,5 @@ def followed_threads(request, course_id, user_id): ...@@ -426,5 +416,5 @@ def followed_threads(request, course_id, user_id):
} }
return render_to_response('discussion/user_profile.html', context) 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 raise Http404
from comment_client import CommentClientError from comment_client import CommentClientError
from django_comment_client.utils import JsonError from django_comment_client.utils import JsonError
import json import json
import logging
log = logging.getLogger(__name__)
class AjaxExceptionMiddleware(object): class AjaxExceptionMiddleware(object):
"""
Middleware that captures CommentClientErrors during ajax requests
and tranforms them into json responses
"""
def process_exception(self, request, exception): 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(): 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 return None
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 string
import random 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 from django.contrib.auth.models import User
from django.test import TestCase
#@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 student.models import CourseEnrollment
from django_comment_client.permissions import has_permission
from django_comment_client.models import Role
class PermissionsTestCase(TestCase): class PermissionsTestCase(TestCase):
......
import string
import random
import collections
from django.test import TestCase from django.test import TestCase
import comment_client import comment_client
...@@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase): ...@@ -13,17 +9,19 @@ class AjaxExceptionTestCase(TestCase):
# TODO: check whether the correct error message is produced. # TODO: check whether the correct error message is produced.
# The error message should be the same as the argument to CommentClientError # The error message should be the same as the argument to CommentClientError
def setUp(self): def setUp(self):
self.a = middleware.AjaxExceptionMiddleware() self.a = middleware.AjaxExceptionMiddleware()
self.request1 = django.http.HttpRequest() self.request1 = django.http.HttpRequest()
self.request0 = django.http.HttpRequest() self.request0 = django.http.HttpRequest()
self.exception1 = comment_client.CommentClientError('{}') self.exception1 = comment_client.CommentClientError('{}')
self.exception0 = ValueError() self.exception2 = comment_client.CommentClientError('Foo!')
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" self.exception0 = ValueError()
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX" self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
def test_process_exception(self): 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.exception1), middleware.JsonError)
self.assertIsNone(self.a.process_exception(self.request1, self.exception0)) self.assertIsInstance(self.a.process_exception(self.request1, self.exception2), middleware.JsonError)
self.assertIsNone(self.a.process_exception(self.request0, self.exception1)) self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
self.assertIsNone(self.a.process_exception(self.request0, 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: ...@@ -8,13 +8,6 @@ Notes for running by hand:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor 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 from django.test.utils import override_settings
# Need access to internal func to put users in the right group # 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, \ ...@@ -26,13 +19,13 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
from django_comment_client.utils import has_forum_access from django_comment_client.utils import has_forum_access
from courseware.access import _course_staff_group_name 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 from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
''' '''
Check for download of csv Check for download of csv
''' '''
...@@ -55,7 +48,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): ...@@ -55,7 +48,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
def make_instructor(course): def make_instructor(course):
group_name = _course_staff_group_name(course.location) group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name) 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) make_instructor(self.toy)
...@@ -63,7 +56,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): ...@@ -63,7 +56,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
self.enroll(self.toy) self.enroll(self.toy)
def test_download_grades_csv(self): def test_download_grades_csv(self):
course = self.toy course = self.toy
url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
...@@ -101,9 +93,8 @@ def action_name(operation, rolename): ...@@ -101,9 +93,8 @@ def action_name(operation, rolename):
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
class TestInstructorDashboardForumAdmin(ct.PageLoader):
''' '''
Check for change in forum admin role memberships Check for change in forum admin role memberships
''' '''
...@@ -112,7 +103,6 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): ...@@ -112,7 +103,6 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses() courses = modulestore().get_courses()
self.course_id = "edX/toy/2012_Fall" self.course_id = "edX/toy/2012_Fall"
self.toy = modulestore().get_course(self.course_id) self.toy = modulestore().get_course(self.course_id)
...@@ -127,14 +117,12 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): ...@@ -127,14 +117,12 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
group_name = _course_staff_group_name(self.toy.location) group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name) 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.logout()
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
self.enroll(self.toy) self.enroll(self.toy)
def initialize_roles(self, course_id): def initialize_roles(self, course_id):
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0] 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] self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
......
"""Tests for License package"""
import logging import logging
import json
from uuid import uuid4 from uuid import uuid4
from random import shuffle from random import shuffle
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from factory import Factory, SubFactory
from django.test import TestCase from django.test import TestCase
from django.core.management import call_command from django.core.management import call_command
from django.core.urlresolvers import reverse
from .models import CourseSoftware, UserLicense from licenses.models import CourseSoftware, UserLicense
from courseware.tests.tests import LoginEnrollmentTestCase, get_user
COURSE_1 = 'edX/toy/2012_Fall' COURSE_1 = 'edX/toy/2012_Fall'
SOFTWARE_1 = 'matlab' SOFTWARE_1 = 'matlab'
SOFTWARE_2 = 'stata' SOFTWARE_2 = 'stata'
SERIAL_1 = '123456abcde'
log = logging.getLogger(__name__) 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): class CommandTest(TestCase):
'''Test management command for importing serial numbers'''
def test_import_serial_numbers(self): def test_import_serial_numbers(self):
size = 20 size = 20
...@@ -51,31 +167,33 @@ class CommandTest(TestCase): ...@@ -51,31 +167,33 @@ class CommandTest(TestCase):
licenses_count = UserLicense.objects.all().count() licenses_count = UserLicense.objects.all().count()
self.assertEqual(3 * size, licenses_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 = list(l.serial for l in lics)
known_serials.extend(generate_serials(10)) known_serials.extend(generate_serials(10))
shuffle(known_serials) shuffle(known_serials)
log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1)) log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
with NamedTemporaryFile() as f: with NamedTemporaryFile() as tmpfile:
f.write('\n'.join(known_serials)) tmpfile.write('\n'.join(known_serials))
f.flush() tmpfile.flush()
args = [COURSE_1, SOFTWARE_1, f.name] args = [COURSE_1, SOFTWARE_1, tmpfile.name]
call_command('import_serial_numbers', *args) call_command('import_serial_numbers', *args)
log.debug('Check if we added only the new ones') 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) self.assertEqual((2 * size) + 10, licenses_count)
def generate_serials(size=20): def generate_serials(size=20):
'''generate a list of serial numbers'''
return [str(uuid4()) for _ in range(size)] return [str(uuid4()) for _ in range(size)]
def generate_serials_file(size=20): def generate_serials_file(size=20):
'''output list of generated serial numbers to a temp file'''
serials = generate_serials(size) serials = generate_serials(size)
temp_file = NamedTemporaryFile() temp_file = NamedTemporaryFile()
......
...@@ -7,12 +7,13 @@ from collections import namedtuple, defaultdict ...@@ -7,12 +7,13 @@ from collections import namedtuple, defaultdict
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse, Http404 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 licenses.models import CourseSoftware
from .models import get_courses_licenses, get_or_create_license, get_license from licenses.models import get_courses_licenses, get_or_create_license, get_license
log = logging.getLogger("mitx.licenses") log = logging.getLogger("mitx.licenses")
...@@ -44,6 +45,7 @@ def get_licenses_by_course(user, courses): ...@@ -44,6 +45,7 @@ def get_licenses_by_course(user, courses):
return data_by_course return data_by_course
@login_required
@requires_csrf_token @requires_csrf_token
def user_software_license(request): def user_software_license(request):
if request.method != 'POST' or not request.is_ajax(): if request.method != 'POST' or not request.is_ajax():
...@@ -65,19 +67,21 @@ def user_software_license(request): ...@@ -65,19 +67,21 @@ def user_software_license(request):
try: try:
software = CourseSoftware.objects.get(name=software_name, software = CourseSoftware.objects.get(name=software_name,
course_id=course_id) course_id=course_id)
print software
except CourseSoftware.DoesNotExist: except CourseSoftware.DoesNotExist:
raise Http404 raise Http404
user = User.objects.get(id=user_id) try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise Http404
if generate: if generate:
license = get_or_create_license(user, software) software_license = get_or_create_license(user, software)
else: else:
license = get_license(user, software) software_license = get_license(user, software)
if license: if software_license:
response = {'serial': license.serial} response = {'serial': software_license.serial}
else: else:
response = {'error': 'No serial number found'} response = {'error': 'No serial number found'}
......
...@@ -4,22 +4,22 @@ Tests for open ended grading interfaces ...@@ -4,22 +4,22 @@ Tests for open ended grading interfaces
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading
""" """
from django.test import TestCase import json
from open_ended_grading import staff_grading_service from mock import MagicMock
from xmodule.open_ended_grading_classes import peer_grading_service
from xmodule import peer_grading_module
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from mitxmako.shortcuts import render_to_string
from courseware.access import _course_staff_group_name from xmodule.open_ended_grading_classes import peer_grading_service
import courseware.tests.tests as ct from xmodule import peer_grading_module
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
from nose import SkipTest
from mock import patch, Mock, MagicMock
import json
from xmodule.x_module import ModuleSystem 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 import logging
...@@ -30,8 +30,8 @@ from django.http import QueryDict ...@@ -30,8 +30,8 @@ from django.http import QueryDict
from xmodule.tests import test_util_open_ended from xmodule.tests import test_util_open_ended
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(ct.PageLoader): class TestStaffGradingService(LoginEnrollmentTestCase):
''' '''
Check that staff grading service proxy works. Basically just checking the Check that staff grading service proxy works. Basically just checking the
access control and error handling logic -- all the actual work is on the access control and error handling logic -- all the actual work is on the
...@@ -56,7 +56,7 @@ class TestStaffGradingService(ct.PageLoader): ...@@ -56,7 +56,7 @@ class TestStaffGradingService(ct.PageLoader):
def make_instructor(course): def make_instructor(course):
group_name = _course_staff_group_name(course.location) group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name) 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) make_instructor(self.toy)
...@@ -126,8 +126,8 @@ class TestStaffGradingService(ct.PageLoader): ...@@ -126,8 +126,8 @@ class TestStaffGradingService(ct.PageLoader):
self.assertIsNotNone(d['problem_list']) self.assertIsNotNone(d['problem_list'])
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestPeerGradingService(ct.PageLoader): class TestPeerGradingService(LoginEnrollmentTestCase):
''' '''
Check that staff grading service proxy works. Basically just checking the Check that staff grading service proxy works. Basically just checking the
access control and error handling logic -- all the actual work is on the access control and error handling logic -- all the actual work is on the
......
...@@ -111,7 +111,7 @@ def peer_grading(request, course_id): ...@@ -111,7 +111,7 @@ def peer_grading(request, course_id):
#Get the peer grading modules currently in the course #Get the peer grading modules currently in the course
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None]) 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) #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 #Get the first one
item_location = items[0].location item_location = items[0].location
#Generate a url for the first module and redirect the user to it #Generate a url for the first module and redirect the user to it
......
...@@ -74,6 +74,15 @@ def to_latex(x): ...@@ -74,6 +74,15 @@ def to_latex(x):
# LatexPrinter._print_dot = _print_dot # LatexPrinter._print_dot = _print_dot
xs = latex(x) xs = latex(x)
xs = xs.replace(r'\XI', 'XI') # workaround for strange greek 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]) #return '<math>%s{}{}</math>' % (xs[1:-1])
if xs[0] == '$': if xs[0] == '$':
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6 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 ...@@ -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) 'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key" 'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1) '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]])'), #'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'), #'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'), #'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
...@@ -247,6 +257,127 @@ class formula(object): ...@@ -247,6 +257,127 @@ class formula(object):
fix_hat(k) fix_hat(k)
fix_hat(xml) 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>&#x200B;</mo>
<mi>d</mi>
</mrow>
</msubsup>
to be interpreted 'a_b__c'
also:
<msup>
<mi>x</mi>
<mrow>
<mo>&#x200B;</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 self.xml = xml
return self.xml return self.xml
...@@ -257,6 +388,7 @@ class formula(object): ...@@ -257,6 +388,7 @@ class formula(object):
try: try:
xml = self.preprocess_pmathml(self.expr) xml = self.preprocess_pmathml(self.expr)
except Exception, err: except Exception, err:
log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr))
return "<html>Error! Cannot process pmathml</html>" return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml, pretty_print=True) pmathml = etree.tostring(xml, pretty_print=True)
self.the_pmathml = pmathml self.the_pmathml = pmathml
......
"""
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>&#x200B;</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>&#x200B;</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)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
left: 0; left: 0;
margin: 100px auto; margin: 100px auto;
top: 0; top: 0;
z-index: 200; height:500px;
} }
.login-box input[type=submit] { .login-box input[type=submit] {
...@@ -18,75 +18,18 @@ ...@@ -18,75 +18,18 @@
height: auto !important; height: auto !important;
} }
#lean_overlay {
display: block;
position: fixed;
left: 0px;
top: 0px;
z-index: 100;
width:100%;
height:100%;
}
</style> </style>
</%block> </%block>
<section id="login-modal" class="modal login-modal login-box"> <section class='login-box'></section>
<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>&#10005;</p>
</div>
</div>
</div>
</section>
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) { $(document).ready(
if(json.success) { function() {
next = getParameterByName('next'); // show dialog
if(next) { $('#login').click();
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");
} }
}); );
})(this) })(this)
</script> </script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment