diff --git a/common/djangoapps/external_auth/djangostore.py b/common/djangoapps/external_auth/djangostore.py new file mode 100644 index 0000000000..3a6848f11f --- /dev/null +++ b/common/djangoapps/external_auth/djangostore.py @@ -0,0 +1,123 @@ +"""A openid store using django cache""" + +from openid.store.interface import OpenIDStore +from openid.store import nonce + +from django.core.cache import cache + +import logging +import time + +DEFAULT_ASSOCIATIONS_TIMEOUT = 60 +DEFAULT_NONCE_TIMEOUT = 600 + +ASSOCIATIONS_KEY_PREFIX = 'openid.provider.associations.' +NONCE_KEY_PREFIX = 'openid.provider.nonce.' + +log = logging.getLogger('DjangoOpenIDStore') + + +def get_url_key(server_url): + key = ASSOCIATIONS_KEY_PREFIX + server_url + return key + + +def get_nonce_key(server_url, timestamp, salt): + key = '{prefix}{url}.{ts}.{salt}'.format(prefix=NONCE_KEY_PREFIX, + url=server_url, + ts=timestamp, + salt=salt) + return key + + +class DjangoOpenIDStore(OpenIDStore): + def __init__(self): + log.info('DjangoStore cache:' + str(cache.__class__)) + + def storeAssociation(self, server_url, assoc): + key = get_url_key(server_url) + + log.info('storeAssociation {0}'.format(key)) + + associations = cache.get(key, {}) + associations[assoc.handle] = assoc + + cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT) + + def getAssociation(self, server_url, handle=None): + key = get_url_key(server_url) + + log.info('getAssociation {0}'.format(key)) + + associations = cache.get(key, {}) + + assoc = None + + if handle is None: + # get best association + valid_assocs = [a for a in associations if a.getExpiresIn() > 0] + if valid_assocs: + valid_assocs.sort(lambda a: a.getExpiresIn(), reverse=True) + assoc = valid_assocs.sort[0] + else: + assoc = associations.get(handle) + + # check expiration and remove if it has expired + if assoc and assoc.getExpiresIn() <= 0: + if handle is None: + cache.delete(key) + else: + associations.pop(handle) + cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT) + assoc = None + + return assoc + + def removeAssociation(self, server_url, handle): + key = get_url_key(server_url) + + log.info('removeAssociation {0}'.format(key)) + + associations = cache.get(key, {}) + + removed = False + + if associations: + if handle is None: + cache.delete(key) + removed = True + else: + assoc = associations.pop(handle) + if assoc: + cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT) + removed = True + + return removed + + def useNonce(self, server_url, timestamp, salt): + key = get_nonce_key(server_url, timestamp, salt) + + log.info('useNonce {0}'.format(key)) + + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + anonce = cache.get(key) + + found = False + + if anonce is None: + cache.set(key, '-', DEFAULT_NONCE_TIMEOUT) + found = False + else: + found = True + + return found + + def cleanupNonces(self): + # not necesary, keys will timeout + return 0 + + def cleanupAssociations(self): + # not necesary, keys will timeout + return 0 diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 520cd95c0b..d456150cf7 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -7,6 +7,7 @@ import string import fnmatch from external_auth.models import ExternalAuthMap +from external_auth.djangostore import DjangoOpenIDStore from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login @@ -30,7 +31,6 @@ from openid.consumer.consumer import SUCCESS from openid.server.server import Server from openid.server.trustroot import TrustRoot -from openid.store.filestore import FileOpenIDStore from openid.extensions import ax, sreg import student.views as student_views @@ -307,10 +307,7 @@ def get_xrds_url(resource, request): """ Return the XRDS url for a resource """ - host = request.META['HTTP_HOST'] - - if not host.endswith('edx.org'): - return None + host = request.get_host() location = host + '/openid/provider/' + resource + '/' @@ -332,6 +329,8 @@ def add_openid_simple_registration(request, response, data): sreg_data['email'] = data['email'] elif field == 'fullname' and 'fullname' in data: sreg_data['fullname'] = data['fullname'] + elif field == 'nickname' and 'nickname' in data: + sreg_data['nickname'] = data['nickname'] # construct sreg response sreg_response = sreg.SRegResponse.extractResponse(sreg_request, @@ -436,7 +435,7 @@ def provider_login(request): return default_render_failure(request, "Invalid OpenID request") # initialize store and server - store = FileOpenIDStore('/tmp/openid_provider') + store = DjangoOpenIDStore() server = Server(store, endpoint) # handle OpenID request @@ -525,13 +524,22 @@ def provider_login(request): url = endpoint + urlquote(user.username) response = openid_request.answer(True, None, url) - return provider_respond(server, - openid_request, - response, - { - 'fullname': profile.name, - 'email': user.email - }) + # TODO: for CS50 we are forcibly returning the username + # instead of fullname. In the OpenID simple registration + # extension, we don't have to return any fields we don't + # want to, even if they were marked as required by the + # Consumer. The behavior of what to do when there are + # missing fields is up to the Consumer. The proper change + # should only return the username, however this will likely + # break the CS50 client. Temporarily we will be returning + # username filling in for fullname in addition to username + # as sreg nickname. + results = { + 'nickname': user.username, + 'email': user.email, + 'fullname': user.username + } + return provider_respond(server, openid_request, response, results) request.session['openid_error'] = True msg = "Login failed - Account not active for user {0}".format(username) diff --git a/common/djangoapps/status/__init__.py b/common/djangoapps/status/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/djangoapps/status/__init__.py @@ -0,0 +1 @@ + diff --git a/common/djangoapps/status/status.py b/common/djangoapps/status/status.py new file mode 100644 index 0000000000..c06a70f5a1 --- /dev/null +++ b/common/djangoapps/status/status.py @@ -0,0 +1,43 @@ +""" +A tiny app that checks for a status message. +""" + +from django.conf import settings +import json +import logging +import os +import sys + +log = logging.getLogger(__name__) + +def get_site_status_msg(course_id): + """ + Look for a file settings.STATUS_MESSAGE_PATH. If found, read it, + parse as json, and do the following: + + * if there is a key 'global', include that in the result list. + * if course is not None, and there is a key for course.id, add that to the result list. + * return "
".join(result) + + Otherwise, return None. + + If something goes wrong, returns None. ("is there a status msg?" logic is + not allowed to break the entire site). + """ + try: + if os.path.isfile(settings.STATUS_MESSAGE_PATH): + with open(settings.STATUS_MESSAGE_PATH) as f: + content = f.read() + else: + return None + + status_dict = json.loads(content) + msg = status_dict.get('global', None) + if course_id in status_dict: + msg = msg + "
" if msg else '' + msg += status_dict[course_id] + + return msg + except: + log.exception("Error while getting a status message.") + return None diff --git a/common/djangoapps/status/tests.py b/common/djangoapps/status/tests.py new file mode 100644 index 0000000000..98a36f433a --- /dev/null +++ b/common/djangoapps/status/tests.py @@ -0,0 +1,90 @@ +from django.conf import settings +from django.test import TestCase +import os +from override_settings import override_settings +from tempfile import NamedTemporaryFile + +from status import get_site_status_msg + +# Get a name where we can put test files +TMP_FILE = NamedTemporaryFile(delete=False) +TMP_NAME = TMP_FILE.name +# Close it--we just want the path. +TMP_FILE.close() + + +@override_settings(STATUS_MESSAGE_PATH=TMP_NAME) +class TestStatus(TestCase): + """Test that the get_site_status_msg function does the right thing""" + + no_file = None + + invalid_json = """{ + "global" : "Hello, Globe", + }""" + + global_only = """{ + "global" : "Hello, Globe" + }""" + + toy_only = """{ + "edX/toy/2012_Fall" : "A toy story" + }""" + + global_and_toy = """{ + "global" : "Hello, Globe", + "edX/toy/2012_Fall" : "A toy story" + }""" + + + # json to use, expected results for course=None (e.g. homepage), + # for toy course, for full course. Note that get_site_status_msg + # is supposed to return global message even if course=None. The + # template just happens to not display it outside the courseware + # at the moment... + checks = [ + (no_file, None, None, None), + (invalid_json, None, None, None), + (global_only, "Hello, Globe", "Hello, Globe", "Hello, Globe"), + (toy_only, None, "A toy story", None), + (global_and_toy, "Hello, Globe", "Hello, Globe
A toy story", "Hello, Globe"), + ] + + def setUp(self): + """ + Fake course ids, since we don't have to have full django + settings (common tests run without the lms settings imported) + """ + self.full_id = 'edX/full/2012_Fall' + self.toy_id = 'edX/toy/2012_Fall' + + def create_status_file(self, contents): + """ + Write contents to settings.STATUS_MESSAGE_PATH. + """ + with open(settings.STATUS_MESSAGE_PATH, 'w') as f: + f.write(contents) + + def remove_status_file(self): + """Delete the status file if it exists""" + if os.path.exists(settings.STATUS_MESSAGE_PATH): + os.remove(settings.STATUS_MESSAGE_PATH) + + def tearDown(self): + self.remove_status_file() + + def test_get_site_status_msg(self): + """run the tests""" + for (json_str, exp_none, exp_toy, exp_full) in self.checks: + + self.remove_status_file() + if json_str: + self.create_status_file(json_str) + + print "checking results for {0}".format(json_str) + print "course=None:" + self.assertEqual(get_site_status_msg(None), exp_none) + print "course=toy:" + self.assertEqual(get_site_status_msg(self.toy_id), exp_toy) + print "course=full:" + self.assertEqual(get_site_status_msg(self.full_id), exp_full) diff --git a/common/djangoapps/student/management/commands/set_staff.py b/common/djangoapps/student/management/commands/set_staff.py new file mode 100644 index 0000000000..5e7df1f585 --- /dev/null +++ b/common/djangoapps/student/management/commands/set_staff.py @@ -0,0 +1,37 @@ +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand, CommandError +import re + + +class Command(BaseCommand): + + args = '' + help = """ + This command will set isstaff to true for one or more users. + Lookup by username or email address, assumes usernames + do not look like email addresses. + """ + + def handle(self, *args, **kwargs): + + if len(args) < 1: + print Command.help + return + + for user in args: + + if re.match('[^@]+@[^@]+\.[^@]+', user): + try: + v = User.objects.get(email=user) + except: + raise CommandError("User {0} does not exist".format( + user)) + else: + try: + v = User.objects.get(username=user) + except: + raise CommandError("User {0} does not exist".format( + user)) + + v.is_staff = True + v.save() diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b19833bbed..b2fcc73ca3 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -262,10 +262,15 @@ def login_user(request, error=""): try_change_enrollment(request) return HttpResponse(json.dumps({'success': True})) - - log.warning("Login failed - Account not active for user {0}".format(username)) + + log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) + + reactivation_email_for_user(user) + not_activated_msg = "This account has not been activated. We have " + \ + "sent another activation message. Please check your " + \ + "e-mail for the activation instructions." return HttpResponse(json.dumps({'success': False, - 'value': 'This account has not been activated. Please check your e-mail for the activation instructions.'})) + 'value': not_activated_msg})) @ensure_csrf_cookie @@ -517,6 +522,17 @@ def password_reset(request): ''' Attempts to send a password reset e-mail. ''' if request.method != "POST": raise Http404 + + # By default, Django doesn't allow Users with is_active = False to reset their passwords, + # but this bites people who signed up a long time ago, never activated, and forgot their + # password. So for their sake, we'll auto-activate a user for whome password_reset is called. + try: + user = User.objects.get(email=request.POST['email']) + user.is_active = True + user.save() + except: + log.exception("Tried to auto-activate user to enable password reset, but failed.") + form = PasswordResetForm(request.POST) if form.is_valid(): form.save(use_https = request.is_secure(), @@ -529,7 +545,6 @@ def password_reset(request): return HttpResponse(json.dumps({'success': False, 'error': 'Invalid e-mail'})) - @ensure_csrf_cookie def reactivation_email(request): ''' Send an e-mail to reactivate a deactivated account, or to @@ -540,25 +555,22 @@ def reactivation_email(request): except User.DoesNotExist: return HttpResponse(json.dumps({'success': False, 'error': 'No inactive user with this e-mail exists'})) + return reactivation_email_for_user(user) - if user.is_active: - return HttpResponse(json.dumps({'success': False, - 'error': 'User is already active'})) - +def reactivation_email_for_user(user): reg = Registration.objects.get(user=user) - reg.register(user) - d = {'name': UserProfile.get(user=user).name, - 'key': r.activation_key} + d = {'name': user.profile.name, + 'key': reg.activation_key} - subject = render_to_string('reactivation_email_subject.txt', d) + subject = render_to_string('emails/activation_email_subject.txt', d) subject = ''.join(subject.splitlines()) - message = render_to_string('reactivation_email.txt', d) + message = render_to_string('emails/activation_email.txt', d) res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) return HttpResponse(json.dumps({'success': True})) - + @ensure_csrf_cookie def change_email_request(request): @@ -642,9 +654,12 @@ def confirm_email_change(request, key): meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()]) up.set_meta(meta) up.save() + # Send it to the old email... + user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) user.email = pec.new_email user.save() pec.delete() + # And send it to the new email... user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) return render_to_response("email_change_successful.html", d) @@ -665,9 +680,12 @@ def change_name_request(request): pnc.rationale = request.POST['rationale'] if len(pnc.new_name) < 2: return HttpResponse(json.dumps({'success': False, 'error': 'Name required'})) - if len(pnc.rationale) < 2: - return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'})) pnc.save() + + # The following automatically accepts name change requests. Remove this to + # go back to the old system where it gets queued up for admin approval. + accept_name_change_by_id(pnc.id) + return HttpResponse(json.dumps({'success': True})) @@ -702,14 +720,9 @@ def reject_name_change(request): return HttpResponse(json.dumps({'success': True})) -@ensure_csrf_cookie -def accept_name_change(request): - ''' JSON: Name change process. Course staff clicks 'accept' on a given name change ''' - if not request.user.is_staff: - raise Http404 - +def accept_name_change_by_id(id): try: - pnc = PendingNameChange.objects.get(id=int(request.POST['id'])) + pnc = PendingNameChange.objects.get(id=id) except PendingNameChange.DoesNotExist: return HttpResponse(json.dumps({'success': False, 'error': 'Invalid ID'})) @@ -728,3 +741,17 @@ def accept_name_change(request): pnc.delete() return HttpResponse(json.dumps({'success': True})) + + +@ensure_csrf_cookie +def accept_name_change(request): + ''' JSON: Name change process. Course staff clicks 'accept' on a given name change + + We used this during the prototype but now we simply record name changes instead + of manually approving them. Still keeping this around in case we want to go + back to this approval method. + ''' + if not request.user.is_staff: + raise Http404 + + return accept_name_change_by_id(int(request.POST['id'])) diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py index 7979a33d84..40ac14308e 100644 --- a/common/lib/capa/capa/calc.py +++ b/common/lib/capa/capa/calc.py @@ -48,7 +48,7 @@ general_whitespace = re.compile('[^\w]+') def check_variables(string, variables): - ''' Confirm the only variables in string are defined. + '''Confirm the only variables in string are defined. Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -56,7 +56,8 @@ def check_variables(string, variables): achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character undefined_variable = achar + Word(alphanums) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) - varnames = varnames | undefined_variable''' + varnames = varnames | undefined_variable + ''' possible_variables = re.split(general_whitespace, string) # List of all alnums in string bad_variables = list() for v in possible_variables: @@ -71,7 +72,8 @@ def check_variables(string, variables): def evaluator(variables, functions, string, cs=False): - ''' Evaluate an expression. Variables are passed as a dictionary + ''' + Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function. Variables must be floats. cs: Case sensitive @@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False): if string.strip() == "": return float('nan') + ops = {"^": operator.pow, "*": operator.mul, "/": operator.truediv, @@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False): def func_parse_action(x): return [all_functions[x[0]](x[1])] - number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent + # SI suffixes and percent + number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") number_part = Word(nums) - inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34 - number = Optional(minus | plus) + inner_number + \ - Optional(CaselessLiteral("E") + Optional("-") + number_part) + \ - Optional(number_suffix) # 0.33k or -17 + + # 0.33 or 7 or .34 + inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) + + # 0.33k or -17 + number = (Optional(minus | plus) + inner_number + + Optional(CaselessLiteral("E") + Optional("-") + number_part) + + Optional(number_suffix)) number = number.setParseAction(number_parse_action) # Convert to number # Predefine recursive variables @@ -201,9 +209,11 @@ def evaluator(variables, functions, string, cs=False): varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) else: varnames = NoMatch() + # Same thing for functions. if len(all_functions) > 0: - funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys())) + funcnames = sreduce(lambda x, y: x | y, + map(lambda x: CasedLiteral(x), all_functions.keys())) function = funcnames + lpar.suppress() + expr + rpar.suppress() function.setParseAction(func_parse_action) else: diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 626ad48c36..d868e66ec5 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -3,8 +3,9 @@ # # Nomenclature: # -# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more -# Input entry fields. The capa Problem may include a solution. +# A capa Problem is a collection of text and capa Response questions. +# Each Response may have one or more Input entry fields. +# The capa problem may include a solution. # ''' Main module which shows problems (of "capa" type). @@ -29,6 +30,8 @@ import sys from lxml import etree from xml.sax.saxutils import unescape +import chem +import chem.chemcalc import calc from correctmap import CorrectMap import eia @@ -42,9 +45,25 @@ import responsetypes # dict of tagname, Response Class -- this should come from auto-registering response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__]) -entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput'] -solution_types = ['solution'] # extra things displayed after "show answers" is pressed -response_properties = ["codeparam", "responseparam", "answer"] # these get captured as student responses +# Different ways students can input code +entry_types = ['textline', + 'schematic', + 'textbox', + 'imageinput', + 'optioninput', + 'choicegroup', + 'radiogroup', + 'checkboxgroup', + 'filesubmission', + 'javascriptinput', + 'crystallography', + 'chemicalequationinput',] + +# extra things displayed after "show answers" is pressed +solution_types = ['solution'] + +# these get captured as student responses +response_properties = ["codeparam", "responseparam", "answer"] # special problem tags which should be turned into innocuous HTML html_transforms = {'problem': {'tag': 'div'}, @@ -57,7 +76,8 @@ global_context = {'random': random, 'math': math, 'scipy': scipy, 'calc': calc, - 'eia': eia} + 'eia': eia, + 'chemcalc': chem.chemcalc} # These should be removed from HTML output, including all subelements html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] @@ -83,7 +103,8 @@ class LoncapaProblem(object): - id (string): identifier for this problem; often a filename (no spaces) - state (dict): student state - seed (int): random number generator seed (int) - - system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context + - system (ModuleSystem): ModuleSystem instance which provides OS, + rendering, and user context ''' @@ -107,19 +128,24 @@ class LoncapaProblem(object): if not self.seed: self.seed = struct.unpack('i', os.urandom(4))[0] - problem_text = re.sub("startouttext\s*/", "text", problem_text) # Convert startouttext and endouttext to proper + # Convert startouttext and endouttext to proper + problem_text = re.sub("startouttext\s*/", "text", problem_text) problem_text = re.sub("endouttext\s*/", "/text", problem_text) self.problem_text = problem_text - self.tree = etree.XML(problem_text) # parse problem XML file into an element tree - self._process_includes() # handle any tags + # parse problem XML file into an element tree + self.tree = etree.XML(problem_text) + + # handle any tags + self._process_includes() # construct script processor context (eg for customresponse problems) self.context = self._extract_context(self.tree, seed=self.seed) - # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations - # this also creates the dict (self.responders) of Response instances for each question in the problem. - # the dict has keys = xml subtree of Response, values = Response instance + # Pre-parse the XML tree: modifies it to add ID's and perform some in-place + # transformations. This also creates the dict (self.responders) of Response + # instances for each question in the problem. The dict has keys = xml subtree of + # Response, values = Response instance self._preprocess_problem(self.tree) if not self.student_answers: # True when student_answers is an empty dict @@ -134,6 +160,9 @@ class LoncapaProblem(object): self.done = False def set_initial_display(self): + """ + Set the student's answers to the responders' initial displays, if specified. + """ initial_answers = dict() for responder in self.responders.values(): if hasattr(responder, 'get_initial_display'): @@ -145,9 +174,11 @@ class LoncapaProblem(object): return u"LoncapaProblem ({0})".format(self.problem_id) def get_state(self): - ''' Stored per-user session data neeeded to: + ''' + Stored per-user session data neeeded to: 1) Recreate the problem - 2) Populate any student answers. ''' + 2) Populate any student answers. + ''' return {'seed': self.seed, 'student_answers': self.student_answers, @@ -156,7 +187,7 @@ class LoncapaProblem(object): def get_max_score(self): ''' - Return maximum score for this problem. + Return the maximum score for this problem. ''' maxscore = 0 for response, responder in self.responders.iteritems(): @@ -164,11 +195,11 @@ class LoncapaProblem(object): return maxscore def get_score(self): - ''' + """ Compute score for this problem. The score is the number of points awarded. Returns a dictionary {'score': integer, from 0 to get_max_score(), 'total': get_max_score()}. - ''' + """ correct = 0 for key in self.correct_map: try: @@ -204,22 +235,25 @@ class LoncapaProblem(object): def is_queued(self): ''' Returns True if any part of the problem has been submitted to an external queue + (e.g. for grading.) ''' return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map) def get_recentmost_queuetime(self): ''' - Returns a DateTime object that represents the timestamp of the most recent queueing request, or None if not queued + Returns a DateTime object that represents the timestamp of the most recent + queueing request, or None if not queued ''' if not self.is_queued(): return None # Get a list of timestamps of all queueing requests, then convert it to a DateTime object queuetime_strs = [self.correct_map.get_queuetime_str(answer_id) - for answer_id in self.correct_map + for answer_id in self.correct_map if self.correct_map.is_queued(answer_id)] - queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) for qt_str in queuetime_strs] + queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) + for qt_str in queuetime_strs] return max(queuetimes) @@ -235,14 +269,20 @@ class LoncapaProblem(object): Calls the Response for each question in this problem, to do the actual grading. ''' + # if answers include File objects, convert them to filenames. self.student_answers = convert_files_to_filenames(answers) - oldcmap = self.correct_map # old CorrectMap - newcmap = CorrectMap() # start new with empty CorrectMap + # old CorrectMap + oldcmap = self.correct_map + + # start new with empty CorrectMap + newcmap = CorrectMap() # log.debug('Responders: %s' % self.responders) - for responder in self.responders.values(): # Call each responsetype instance to do actual grading - if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype - # explicitly allows for file submissions + # Call each responsetype instance to do actual grading + for responder in self.responders.values(): + # File objects are passed only if responsetype explicitly allows for file + # submissions + if 'filesubmission' in responder.allowed_inputfields: results = responder.evaluate_answers(answers, oldcmap) else: results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap) @@ -252,28 +292,33 @@ class LoncapaProblem(object): return newcmap def get_question_answers(self): - """Returns a dict of answer_ids to answer values. If we cannot generate + """ + Returns a dict of answer_ids to answer values. If we cannot generate an answer (this sometimes happens in customresponses), that answer_id is not included. Called by "show answers" button JSON request (see capa_module) """ + # dict of (id, correct_answer) answer_map = dict() for response in self.responders.keys(): results = self.responder_answers[response] - answer_map.update(results) # dict of (id,correct_answer) + answer_map.update(results) # include solutions from ... stanzas for entry in self.tree.xpath("//" + "|//".join(solution_types)): answer = etree.tostring(entry) - if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context) + if answer: + answer_map[entry.get('id')] = contextualize_text(answer, self.context) log.debug('answer_map = %s' % answer_map) return answer_map def get_answer_ids(self): - """Return the IDs of all the responses -- these are the keys used for + """ + Return the IDs of all the responses -- these are the keys used for the dicts returned by grade_answers and get_question_answers. (Though - get_question_answers may only return a subset of these.""" + get_question_answers may only return a subset of these. + """ answer_ids = [] for response in self.responders.keys(): results = self.responder_answers[response] @@ -298,7 +343,8 @@ class LoncapaProblem(object): file = inc.get('file') if file is not None: try: - ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore + # open using ModuleSystem OSFS filestore + ifp = self.system.filestore.open(file) except Exception as err: log.warning('Error %s in problem xml include: %s' % ( err, etree.tostring(inc, pretty_print=True))) @@ -311,7 +357,8 @@ class LoncapaProblem(object): else: continue try: - incxml = etree.XML(ifp.read()) # read in and convert to XML + # read in and convert to XML + incxml = etree.XML(ifp.read()) except Exception as err: log.warning('Error %s in problem xml include: %s' % ( err, etree.tostring(inc, pretty_print=True))) @@ -322,6 +369,7 @@ class LoncapaProblem(object): raise else: continue + # insert new XML into tree in place of inlcude parent = inc.getparent() parent.insert(parent.index(inc), incxml) @@ -329,11 +377,13 @@ class LoncapaProblem(object): log.debug('Included %s into %s' % (file, self.problem_id)) def _extract_system_path(self, script): - ''' + """ Extracts and normalizes additional paths for code execution. For now, there's a default path of data/course/code; this may be removed at some point. - ''' + + script : ?? (TODO) + """ DEFAULT_PATH = ['code'] @@ -351,7 +401,6 @@ class LoncapaProblem(object): # path is an absolute path or a path relative to the data dir dir = os.path.join(self.system.filestore.root_path, dir) abs_dir = os.path.normpath(dir) - #log.debug("appending to path: %s" % abs_dir) path.append(abs_dir) return path @@ -362,13 +411,20 @@ class LoncapaProblem(object): context of this problem. Provides ability to randomize problems, and also set variables for problem answer checking. - Problem XML goes to Python execution context. Runs everything in script tags + Problem XML goes to Python execution context. Runs everything in script tags. ''' random.seed(self.seed) - context = {'global_context': global_context} # save global context in here also - context.update(global_context) # initialize context to have stuff in global_context - context['__builtins__'] = globals()['__builtins__'] # put globals there also - context['the_lcp'] = self # pass instance of LoncapaProblem in + # save global context in here also + context = {'global_context': global_context} + + # initialize context to have stuff in global_context + context.update(global_context) + + # put globals there also + context['__builtins__'] = globals()['__builtins__'] + + # pass instance of LoncapaProblem in + context['the_lcp'] = self context['script_code'] = '' self._execute_scripts(tree.findall('.//script'), context) @@ -385,7 +441,7 @@ class LoncapaProblem(object): sys.path = original_path + self._extract_system_path(script) stype = script.get('type') - + if stype: if 'javascript' in stype: continue # skip javascript @@ -395,12 +451,14 @@ class LoncapaProblem(object): code = script.text XMLESC = {"'": "'", """: '"'} code = unescape(code, XMLESC) - context['script_code'] += code # store code source in context + # store code source in context + context['script_code'] += code try: - exec code in context, context # use "context" for global context; thus defs in code are global within code + # use "context" for global context; thus defs in code are global within code + exec code in context, context except Exception as err: log.exception("Error while execing script code: " + code) - msg = "Error while executing script code: %s" % str(err).replace('<','<') + msg = "Error while executing script code: %s" % str(err).replace('<','<') raise responsetypes.LoncapaProblemError(msg) finally: sys.path = original_path @@ -415,7 +473,8 @@ class LoncapaProblem(object): Used by get_html. ''' - if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'): + if (problemtree.tag == 'script' and problemtree.get('type') + and 'javascript' in problemtree.get('type')): # leave javascript intact. return problemtree @@ -424,8 +483,8 @@ class LoncapaProblem(object): problemid = problemtree.get('id') # my ID - if problemtree.tag in inputtypes.get_input_xml_tags(): - + if problemtree.tag in inputtypes.registered_input_tags(): + # If this is an inputtype subtree, let it render itself. status = "unsubmitted" msg = '' hint = '' @@ -442,32 +501,34 @@ class LoncapaProblem(object): value = self.student_answers[problemid] # do the rendering - render_object = inputtypes.SimpleInput(system=self.system, - xml=problemtree, - state={'value': value, - 'status': status, - 'id': problemtree.get('id'), - 'feedback': {'message': msg, - 'hint': hint, - 'hintmode': hintmode, - } - }, - use='capa_input') - return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) - if problemtree in self.responders: # let each Response render itself + state = {'value': value, + 'status': status, + 'id': problemtree.get('id'), + 'feedback': {'message': msg, + 'hint': hint, + 'hintmode': hintmode,}} + + input_type_cls = inputtypes.get_class_for_tag(problemtree.tag) + the_input = input_type_cls(self.system, problemtree, state) + return the_input.get_html() + + # let each Response render itself + if problemtree in self.responders: return self.responders[problemtree].render_html(self._extract_html) tree = etree.Element(problemtree.tag) for item in problemtree: - item_xhtml = self._extract_html(item) # nothing special: recurse + # render child recursively + item_xhtml = self._extract_html(item) if item_xhtml is not None: tree.append(item_xhtml) if tree.tag in html_transforms: tree.tag = html_transforms[problemtree.tag]['tag'] else: - for (key, value) in problemtree.items(): # copy attributes over if not innocufying + # copy attributes over if not innocufying + for (key, value) in problemtree.items(): tree.set(key, value) tree.text = problemtree.text @@ -490,31 +551,41 @@ class LoncapaProblem(object): self.responders = {} for response in tree.xpath('//' + "|//".join(response_tag_dict)): response_id_str = self.problem_id + "_" + str(response_id) - response.set('id', response_id_str) # create and save ID for this response + # create and save ID for this response + response.set('id', response_id_str) response_id += 1 answer_id = 1 - inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), + inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x + for x in (entry_types + solution_types)]), id=response_id_str) - for entry in inputfields: # assign one answer_id for each entry_type or solution_type + + # assign one answer_id for each entry_type or solution_type + for entry in inputfields: entry.attrib['response_id'] = str(response_id) entry.attrib['answer_id'] = str(answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) answer_id = answer_id + 1 - responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response - self.responders[response] = responder # save in list in self + # instantiate capa Response + responder = response_tag_dict[response.tag](response, inputfields, + self.context, self.system) + # save in list in self + self.responders[response] = responder - # get responder answers (do this only once, since there may be a performance cost, eg with externalresponse) + # get responder answers (do this only once, since there may be a performance cost, + # eg with externalresponse) self.responder_answers = {} for response in self.responders.keys(): try: self.responder_answers[response] = self.responders[response].get_answers() except: - log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME + log.debug('responder %s failed to properly return get_answers()', + self.responders[response]) # FIXME raise - # ... may not be associated with any specific response; give IDs for those separately + # ... may not be associated with any specific response; give + # IDs for those separately # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). solution_id = 1 for solution in tree.findall('.//solution'): diff --git a/common/lib/capa/capa/chem/__init__.py b/common/lib/capa/capa/chem/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/lib/capa/capa/chem/__init__.py @@ -0,0 +1 @@ + diff --git a/common/lib/capa/capa/chem/chemcalc.py b/common/lib/capa/capa/chem/chemcalc.py new file mode 100644 index 0000000000..389e688cf4 --- /dev/null +++ b/common/lib/capa/capa/chem/chemcalc.py @@ -0,0 +1,433 @@ +from __future__ import division +import copy +from fractions import Fraction +import logging +import math +import operator +import re +import numpy +import numbers +import scipy.constants + +from pyparsing import (Literal, Keyword, Word, nums, StringEnd, Optional, + Forward, OneOrMore, ParseException) +import nltk +from nltk.tree import Tree + +ARROWS = ('<->', '->') + +## Defines a simple pyparsing tokenizer for chemical equations +elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be', + 'Bh','Bi','Bk','Br','C','Ca','Cd','Ce','Cf','Cl','Cm', + 'Cn','Co','Cr','Cs','Cu','Db','Ds','Dy','Er','Es','Eu', + 'F','Fe','Fl','Fm','Fr','Ga','Gd','Ge','H','He','Hf', + 'Hg','Ho','Hs','I','In','Ir','K','Kr','La','Li','Lr', + 'Lu','Lv','Md','Mg','Mn','Mo','Mt','N','Na','Nb','Nd', + 'Ne','Ni','No','Np','O','Os','P','Pa','Pb','Pd','Pm', + 'Po','Pr','Pt','Pu','Ra','Rb','Re','Rf','Rg','Rh','Rn', + 'Ru','S','Sb','Sc','Se','Sg','Si','Sm','Sn','Sr','Ta', + 'Tb','Tc','Te','Th','Ti','Tl','Tm','U','Uuo','Uup', + 'Uus','Uut','V','W','Xe','Y','Yb','Zn','Zr'] +digits = map(str, range(10)) +symbols = list("[](){}^+-/") +phases = ["(s)", "(l)", "(g)", "(aq)"] +tokens = reduce(lambda a, b: a ^ b, map(Literal, elements + digits + symbols + phases)) +tokenizer = OneOrMore(tokens) + StringEnd() + + +def _orjoin(l): + return "'" + "' | '".join(l) + "'" + +## Defines an NLTK parser for tokenized expressions +grammar = """ + S -> multimolecule | multimolecule '+' S + multimolecule -> count molecule | molecule + count -> number | number '/' number + molecule -> unphased | unphased phase + unphased -> group | paren_group_round | paren_group_square + element -> """ + _orjoin(elements) + """ + digit -> """ + _orjoin(digits) + """ + phase -> """ + _orjoin(phases) + """ + number -> digit | digit number + group -> suffixed | suffixed group + paren_group_round -> '(' group ')' + paren_group_square -> '[' group ']' + plus_minus -> '+' | '-' + number_suffix -> number + ion_suffix -> '^' number plus_minus | '^' plus_minus + suffix -> number_suffix | number_suffix ion_suffix | ion_suffix + unsuffixed -> element | paren_group_round | paren_group_square + + suffixed -> unsuffixed | unsuffixed suffix +""" +parser = nltk.ChartParser(nltk.parse_cfg(grammar)) + + +def _clean_parse_tree(tree): + ''' The parse tree contains a lot of redundant + nodes. E.g. paren_groups have groups as children, etc. This will + clean up the tree. + ''' + def unparse_number(n): + ''' Go from a number parse tree to a number ''' + if len(n) == 1: + rv = n[0][0] + else: + rv = n[0][0] + unparse_number(n[1]) + return rv + + def null_tag(n): + ''' Remove a tag ''' + return n[0] + + def ion_suffix(n): + '''1. "if" part handles special case + 2. "else" part is general behaviour ''' + + if n[1:][0].node == 'number' and n[1:][0][0][0] == '1': + # if suffix is explicitly 1, like ^1- + # strip 1, leave only sign: ^- + return nltk.tree.Tree(n.node, n[2:]) + else: + return nltk.tree.Tree(n.node, n[1:]) + + dispatch = {'number': lambda x: nltk.tree.Tree("number", [unparse_number(x)]), + 'unphased': null_tag, + 'unsuffixed': null_tag, + 'number_suffix': lambda x: nltk.tree.Tree('number_suffix', [unparse_number(x[0])]), + 'suffixed': lambda x: len(x) > 1 and x or x[0], + 'ion_suffix': ion_suffix, + 'paren_group_square': lambda x: nltk.tree.Tree(x.node, x[1]), + 'paren_group_round': lambda x: nltk.tree.Tree(x.node, x[1])} + + if type(tree) == str: + return tree + + old_node = None + ## This loop means that if a node is processed, and returns a child, + ## the child will be processed. + while tree.node in dispatch and tree.node != old_node: + old_node = tree.node + tree = dispatch[tree.node](tree) + + children = [] + for child in tree: + child = _clean_parse_tree(child) + children.append(child) + + tree = nltk.tree.Tree(tree.node, children) + + return tree + + +def _merge_children(tree, tags): + ''' nltk, by documentation, cannot do arbitrary length + groups. Instead of: + (group 1 2 3 4) + It has to handle this recursively: + (group 1 (group 2 (group 3 (group 4)))) + We do the cleanup of converting from the latter to the former. + ''' + if tree is None: + # There was a problem--shouldn't have empty trees (NOTE: see this with input e.g. 'H2O(', or 'Xe+'). + # Haven't grokked the code to tell if this is indeed the right thing to do. + raise ParseException("Shouldn't have empty trees") + + if type(tree) == str: + return tree + + merged_children = [] + done = False + #print '00000', tree + ## Merge current tag + while not done: + done = True + for child in tree: + if type(child) == nltk.tree.Tree and child.node == tree.node and tree.node in tags: + merged_children = merged_children + list(child) + done = False + else: + merged_children = merged_children + [child] + tree = nltk.tree.Tree(tree.node, merged_children) + merged_children = [] + #print '======',tree + + # And recurse + children = [] + for child in tree: + children.append(_merge_children(child, tags)) + + #return tree + return nltk.tree.Tree(tree.node, children) + + +def _render_to_html(tree): + ''' Renders a cleaned tree to HTML ''' + + def molecule_count(tree, children): + # If an integer, return that integer + if len(tree) == 1: + return tree[0][0] + # If a fraction, return the fraction + if len(tree) == 3: + return " {num}{den} ".format(num=tree[0][0], den=tree[2][0]) + return "Error" + + def subscript(tree, children): + return "{sub}".format(sub=children) + + def superscript(tree, children): + return "{sup}".format(sup=children) + + def round_brackets(tree, children): + return "({insider})".format(insider=children) + + def square_brackets(tree, children): + return "[{insider}]".format(insider=children) + + dispatch = {'count': molecule_count, + 'number_suffix': subscript, + 'ion_suffix': superscript, + 'paren_group_round': round_brackets, + 'paren_group_square': square_brackets} + + if type(tree) == str: + return tree + else: + children = "".join(map(_render_to_html, tree)) + if tree.node in dispatch: + return dispatch[tree.node](tree, children) + else: + return children.replace(' ', '') + + + +def render_to_html(eq): + ''' + Render a chemical equation string to html. + + Renders each molecule separately, and returns invalid input wrapped in a . + ''' + def err(s): + "Render as an error span" + return '{0}'.format(s) + + def render_arrow(arrow): + """Turn text arrows into pretty ones""" + if arrow == '->': + return u'\u2192' + if arrow == '<->': + return u'\u2194' + + # this won't be reached unless we add more arrow types, but keep it to avoid explosions when + # that happens. + return arrow + + def render_expression(ex): + """ + Render a chemical expression--no arrows. + """ + try: + return _render_to_html(_get_final_tree(ex)) + except ParseException: + return err(ex) + + def spanify(s): + return u'{0}'.format(s) + + left, arrow, right = split_on_arrow(eq) + if arrow == '': + # only one side + return spanify(render_expression(left)) + + + return spanify(render_expression(left) + render_arrow(arrow) + render_expression(right)) + + +def _get_final_tree(s): + ''' + Return final tree after merge and clean. + + Raises pyparsing.ParseException if s is invalid. + ''' + tokenized = tokenizer.parseString(s) + parsed = parser.parse(tokenized) + merged = _merge_children(parsed, {'S','group'}) + final = _clean_parse_tree(merged) + return final + + +def _check_equality(tuple1, tuple2): + ''' return True if tuples of multimolecules are equal ''' + list1 = list(tuple1) + list2 = list(tuple2) + + # Hypo: trees where are levels count+molecule vs just molecule + # cannot be sorted properly (tested on test_complex_additivity) + # But without factors and phases sorting seems to work. + + # Also for lists of multimolecules without factors and phases + # sorting seems to work fine. + list1.sort() + list2.sort() + return list1 == list2 + + +def compare_chemical_expression(s1, s2, ignore_state=False): + ''' It does comparison between two expressions. + It uses divide_chemical_expression and check if division is 1 + ''' + return divide_chemical_expression(s1, s2, ignore_state) == 1 + + +def divide_chemical_expression(s1, s2, ignore_state=False): + '''Compare two chemical expressions for equivalence up to a multiplicative factor: + + - If they are not the same chemicals, returns False. + - If they are the same, "divide" s1 by s2 to returns a factor x such that s1 / s2 == x as a Fraction object. + - if ignore_state is True, ignores phases when doing the comparison. + + Examples: + divide_chemical_expression("H2O", "3H2O") -> Fraction(1,3) + divide_chemical_expression("3H2O", "H2O") -> 3 # actually Fraction(3, 1), but compares == to 3. + divide_chemical_expression("2H2O(s) + 2CO2", "H2O(s)+CO2") -> 2 + divide_chemical_expression("H2O(s) + CO2", "3H2O(s)+2CO2") -> False + + Implementation sketch: + - extract factors and phases to standalone lists, + - compare expressions without factors and phases, + - divide lists of factors for each other and check + for equality of every element in list, + - return result of factor division + + ''' + + # parsed final trees + treedic = {} + treedic['1'] = _get_final_tree(s1) + treedic['2'] = _get_final_tree(s2) + + # strip phases and factors + # collect factors in list + for i in ('1', '2'): + treedic[i + ' cleaned_mm_list'] = [] + treedic[i + ' factors'] = [] + treedic[i + ' phases'] = [] + for el in treedic[i].subtrees(filter=lambda t: t.node == 'multimolecule'): + count_subtree = [t for t in el.subtrees() if t.node == 'count'] + group_subtree = [t for t in el.subtrees() if t.node == 'group'] + phase_subtree = [t for t in el.subtrees() if t.node == 'phase'] + if count_subtree: + if len(count_subtree[0]) > 1: + treedic[i + ' factors'].append( + int(count_subtree[0][0][0]) / + int(count_subtree[0][2][0])) + else: + treedic[i + ' factors'].append(int(count_subtree[0][0][0])) + else: + treedic[i + ' factors'].append(1.0) + if phase_subtree: + treedic[i + ' phases'].append(phase_subtree[0][0]) + else: + treedic[i + ' phases'].append(' ') + treedic[i + ' cleaned_mm_list'].append( + Tree('multimolecule', [Tree('molecule', group_subtree)])) + + # order of factors and phases must mirror the order of multimolecules, + # use 'decorate, sort, undecorate' pattern + treedic['1 cleaned_mm_list'], treedic['1 factors'], treedic['1 phases'] = zip( + *sorted(zip(treedic['1 cleaned_mm_list'], treedic['1 factors'], treedic['1 phases']))) + + treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'] = zip( + *sorted(zip(treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases']))) + + # check if expressions are correct without factors + if not _check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']): + return False + + # phases are ruled by ingore_state flag + if not ignore_state: # phases matters + if treedic['1 phases'] != treedic['2 phases']: + return False + + if any(map(lambda x, y: x / y - treedic['1 factors'][0] / treedic['2 factors'][0], + treedic['1 factors'], treedic['2 factors'])): + # factors are not proportional + return False + else: + # return ratio + return Fraction(treedic['1 factors'][0] / treedic['2 factors'][0]) + + +def split_on_arrow(eq): + """ + Split a string on an arrow. Returns left, arrow, right. If there is no arrow, returns the + entire eq in left, and '' in arrow and right. + + Return left, arrow, right. + """ + # order matters -- need to try <-> first + for arrow in ARROWS: + left, a, right = eq.partition(arrow) + if a != '': + return left, a, right + + return eq, '', '' + + +def chemical_equations_equal(eq1, eq2, exact=False): + """ + Check whether two chemical equations are the same. (equations have arrows) + + If exact is False, then they are considered equal if they differ by a + constant factor. + + arrows matter: -> and <-> are different. + + e.g. + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 -> H2O2') -> True + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + 2H2 -> H2O2') -> False + + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 <-> H2O2') -> False + + chemical_equations_equal('H2 + O2 -> H2O2', '2 H2 + 2 O2 -> 2 H2O2') -> True + chemical_equations_equal('H2 + O2 -> H2O2', '2 H2 + 2 O2 -> 2 H2O2', exact=True) -> False + + + If there's a syntax error, we return False. + """ + + left1, arrow1, right1 = split_on_arrow(eq1) + left2, arrow2, right2 = split_on_arrow(eq2) + + if arrow1 == '' or arrow2 == '': + return False + + # TODO: may want to be able to give student helpful feedback about why things didn't work. + if arrow1 != arrow2: + # arrows don't match + return False + + try: + factor_left = divide_chemical_expression(left1, left2) + if not factor_left: + # left sides don't match + return False + + factor_right = divide_chemical_expression(right1, right2) + if not factor_right: + # right sides don't match + return False + + if factor_left != factor_right: + # factors don't match (molecule counts to add up) + return False + + if exact and factor_left != 1: + # want an exact match. + return False + + return True + except ParseException: + # Don't want external users to have to deal with parsing exceptions. Just return False. + return False diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py new file mode 100644 index 0000000000..34d903ec1d --- /dev/null +++ b/common/lib/capa/capa/chem/tests.py @@ -0,0 +1,336 @@ +import codecs +from fractions import Fraction +from pyparsing import ParseException +import unittest + +from chemcalc import (compare_chemical_expression, divide_chemical_expression, + render_to_html, chemical_equations_equal) + +local_debug = None + +def log(s, output_type=None): + if local_debug: + print s + if output_type == 'html': + f.write(s + '\n
\n') + + +class Test_Compare_Equations(unittest.TestCase): + def test_simple_equation(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> H2O2')) + # left sides don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + 2H2 -> H2O2')) + # right sides don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> H2O')) + + # factors don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> 2H2O2')) + + def test_different_factor(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('2H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + + def test_different_arrows(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 <-> 2H2O2')) + + def test_exact_match(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2', exact=True)) + + # order still doesn't matter + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> H2O2', exact=True)) + + + def test_syntax_errors(self): + self.assertFalse(chemical_equations_equal('H2 + O2 a-> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('H2O( -> H2O2', + 'H2O -> H2O2')) + + + self.assertFalse(chemical_equations_equal('H2 + O2 ==> H2O2', # strange arrow + '2O2 + 2H2 -> 2H2O2')) + + +class Test_Compare_Expressions(unittest.TestCase): + + def test_compare_incorrect_order_of_atoms_in_molecule(self): + self.assertFalse(compare_chemical_expression("H2O + CO2", "O2C + OH2")) + + def test_compare_same_order_no_phases_no_factors_no_ions(self): + self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2+H2O")) + + def test_compare_different_order_no_phases_no_factors_no_ions(self): + self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2 + H2O")) + + def test_compare_different_order_three_multimolecule(self): + self.assertTrue(compare_chemical_expression("H2O + Fe(OH)3 + CO2", "CO2 + H2O + Fe(OH)3")) + + def test_compare_same_factors(self): + self.assertTrue(compare_chemical_expression("3H2O + 2CO2", "2CO2 + 3H2O ")) + + def test_compare_different_factors(self): + self.assertFalse(compare_chemical_expression("2H2O + 3CO2", "2CO2 + 3H2O ")) + + def test_compare_correct_ions(self): + self.assertTrue(compare_chemical_expression("H^+ + OH^-", " OH^- + H^+ ")) + + def test_compare_wrong_ions(self): + self.assertFalse(compare_chemical_expression("H^+ + OH^-", " OH^- + H^- ")) + + def test_compare_parent_groups_ions(self): + self.assertTrue(compare_chemical_expression("Fe(OH)^2- + (OH)^-", " (OH)^- + Fe(OH)^2- ")) + + def test_compare_correct_factors_ions_and_one(self): + self.assertTrue(compare_chemical_expression("3H^+ + 2OH^-", " 2OH^- + 3H^+ ")) + + def test_compare_wrong_factors_ions(self): + self.assertFalse(compare_chemical_expression("2H^+ + 3OH^-", " 2OH^- + 3H^+ ")) + + def test_compare_float_factors(self): + self.assertTrue(compare_chemical_expression("7/2H^+ + 3/5OH^-", " 3/5OH^- + 7/2H^+ ")) + + # Phases tests + def test_compare_phases_ignored(self): + self.assertTrue(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2", ignore_state=True)) + + def test_compare_phases_not_ignored_explicitly(self): + self.assertFalse(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2", ignore_state=False)) + + def test_compare_phases_not_ignored(self): # same as previous + self.assertFalse(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2")) + + def test_compare_phases_not_ignored_explicitly(self): + self.assertTrue(compare_chemical_expression( + "H2O(s) + CO2", "H2O(s)+CO2", ignore_state=False)) + + # all in one cases + def test_complex_additivity(self): + self.assertTrue(compare_chemical_expression( + "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", + "7/2HCl + 2H20 + H2O + 5(H1H212)^70010-")) + + def test_complex_additivity_wrong(self): + self.assertFalse(compare_chemical_expression( + "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", + "2H20 + 7/2HCl + H2O + 5(H1H212)^70011-")) + + def test_complex_all_grammar(self): + self.assertTrue(compare_chemical_expression( + "5[Ni(NH3)4]^2+ + 5/2SO4^2-", + "5/2SO4^2- + 5[Ni(NH3)4]^2+")) + + # special cases + + def test_compare_one_superscript_explicitly_set(self): + self.assertTrue(compare_chemical_expression("H^+ + OH^1-", " OH^- + H^+ ")) + + def test_compare_equal_factors_differently_set(self): + self.assertTrue(compare_chemical_expression("6/2H^+ + OH^-", " OH^- + 3H^+ ")) + + def test_compare_one_subscript_explicitly_set(self): + self.assertFalse(compare_chemical_expression("H2 + CO2", "H2 + C102")) + + +class Test_Divide_Expressions(unittest.TestCase): + ''' as compare_ use divide_, + tests here must consider different + division (not equality) cases ''' + + def test_divide_by_zero(self): + self.assertFalse(divide_chemical_expression( + "0H2O", "H2O")) + + def test_divide_wrong_factors(self): + self.assertFalse(divide_chemical_expression( + "5(H1H212)^70010- + 10H2O", "5H2O + 10(H1H212)^70010-")) + + def test_divide_right(self): + self.assertEqual(divide_chemical_expression( + "5(H1H212)^70010- + 10H2O", "10H2O + 5(H1H212)^70010-"), 1) + + def test_divide_wrong_reagents(self): + self.assertFalse(divide_chemical_expression( + "H2O + CO2", "CO2")) + + def test_divide_right_simple(self): + self.assertEqual(divide_chemical_expression( + "H2O + CO2", "H2O+CO2"), 1) + + def test_divide_right_phases(self): + self.assertEqual(divide_chemical_expression( + "H2O(s) + CO2", "2H2O(s)+2CO2"), Fraction(1, 2)) + + def test_divide_right_phases_other_order(self): + self.assertEqual(divide_chemical_expression( + "2H2O(s) + 2CO2", "H2O(s)+CO2"), 2) + + def test_divide_wrong_phases(self): + self.assertFalse(divide_chemical_expression( + "H2O(s) + CO2", "2H2O+2CO2(s)")) + + def test_divide_wrong_phases_but_phases_ignored(self): + self.assertEqual(divide_chemical_expression( + "H2O(s) + CO2", "2H2O+2CO2(s)", ignore_state=True), Fraction(1, 2)) + + def test_divide_order(self): + self.assertEqual(divide_chemical_expression( + "2CO2 + H2O", "2H2O+4CO2"), Fraction(1, 2)) + + def test_divide_fract_to_int(self): + self.assertEqual(divide_chemical_expression( + "3/2CO2 + H2O", "2H2O+3CO2"), Fraction(1, 2)) + + def test_divide_fract_to_frac(self): + self.assertEqual(divide_chemical_expression( + "3/4CO2 + H2O", "2H2O+9/6CO2"), Fraction(1, 2)) + + def test_divide_fract_to_frac_wrog(self): + self.assertFalse(divide_chemical_expression( + "6/2CO2 + H2O", "2H2O+9/6CO2"), 2) + + +class Test_Render_Equations(unittest.TestCase): + + def test_render1(self): + s = "H2O + CO2" + out = render_to_html(s) + correct = u'H2O+CO2' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_uncorrect_reaction(self): + s = "O2C + OH2" + out = render_to_html(s) + correct = u'O2C+OH2' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render2(self): + s = "CO2 + H2O + Fe(OH)3" + out = render_to_html(s) + correct = u'CO2+H2O+Fe(OH)3' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render3(self): + s = "3H2O + 2CO2" + out = render_to_html(s) + correct = u'3H2O+2CO2' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render4(self): + s = "H^+ + OH^-" + out = render_to_html(s) + correct = u'H++OH-' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render5(self): + s = "Fe(OH)^2- + (OH)^-" + out = render_to_html(s) + correct = u'Fe(OH)2-+(OH)-' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render6(self): + s = "7/2H^+ + 3/5OH^-" + out = render_to_html(s) + correct = u'72H++35OH-' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render7(self): + s = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O" + out = render_to_html(s) + correct = u'5(H1H212)70010-+2H2O+72HCl+H2O' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render8(self): + s = "H2O(s) + CO2" + out = render_to_html(s) + correct = u'H2O(s)+CO2' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render9(self): + s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" + #import ipdb; ipdb.set_trace() + out = render_to_html(s) + correct = u'5[Ni(NH3)4]2++52SO42-' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_error(self): + s = "5.2H20" + out = render_to_html(s) + correct = u'5.2H20' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_simple_brackets(self): + s = "(Ar)" + out = render_to_html(s) + correct = u'(Ar)' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_eq1(self): + s = "H^+ + OH^- -> H2O" + out = render_to_html(s) + correct = u'H++OH-\u2192H2O' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_eq2(self): + s = "H^+ + OH^- <-> H2O" + out = render_to_html(s) + correct = u'H++OH-\u2194H2O' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + + def test_render_eq3(self): + s = "H^+ + OH^- <= H2O" # unsupported arrow + out = render_to_html(s) + correct = u'H^+ + OH^- <= H2O' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + + +def suite(): + + testcases = [Test_Compare_Expressions, Test_Divide_Expressions, Test_Render_Equations] + suites = [] + for testcase in testcases: + suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) + return unittest.TestSuite(suites) + +if __name__ == "__main__": + local_debug = True + with codecs.open('render.html', 'w', encoding='utf-8') as f: + unittest.TextTestRunner(verbosity=2).run(suite()) + # open render.html to look at rendered equations diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 52411a8e8c..227f85bc8e 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -5,23 +5,26 @@ class CorrectMap(object): - ''' + """ Stores map between answer_id and response evaluation result for each question in a capa problem. The response evaluation result for each answer_id includes (correctness, npoints, msg, hint, hintmode). - correctness : either 'correct' or 'incorrect' - npoints : None, or integer specifying number of points awarded for this answer_id - - msg : string (may have HTML) giving extra message response (displayed below textline or textbox) - - hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) + - msg : string (may have HTML) giving extra message response + (displayed below textline or textbox) + - hint : string (may have HTML) giving optional hint + (displayed below textline or textbox, above msg) - hintmode : one of (None,'on_request','always') criteria for displaying hint - queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued Behaves as a dict. - ''' + """ def __init__(self, *args, **kwargs): - self.cmap = dict() # start with empty dict + # start with empty dict + self.cmap = dict() self.items = self.cmap.items self.keys = self.cmap.keys self.set(*args, **kwargs) @@ -33,7 +36,15 @@ class CorrectMap(object): return self.cmap.__iter__() # See the documentation for 'set_dict' for the use of kwargs - def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs): + def set(self, + answer_id=None, + correctness=None, + npoints=None, + msg='', + hint='', + hintmode=None, + queuestate=None, **kwargs): + if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, 'npoints': npoints, @@ -56,12 +67,13 @@ class CorrectMap(object): ''' Set internal dict of CorrectMap to provided correct_map dict - correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that - when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict - not coincide with the newest CorrectMap format as defined by self.set. + correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This + means that when the definition of CorrectMap (e.g. its properties) are altered, + an existing correct_map dict not coincide with the newest CorrectMap format as + defined by self.set. For graceful migration, feed the contents of each correct map to self.set, rather than - making a direct copy of the given correct_map dict. This way, the common keys between + making a direct copy of the given correct_map dict. This way, the common keys between the incoming correct_map dict and the new CorrectMap instance will be written, while mismatched keys will be gracefully ignored. @@ -69,14 +81,20 @@ class CorrectMap(object): If correct_map is a one-level dict, then convert it to the new dict of dicts format. ''' if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): - self.__init__() # empty current dict - for k in correct_map: self.set(k, correct_map[k]) # create new dict entries + # empty current dict + self.__init__() + + # create new dict entries + for k in correct_map: + self.set(k, correct_map[k]) else: self.__init__() - for k in correct_map: self.set(k, **correct_map[k]) + for k in correct_map: + self.set(k, **correct_map[k]) def is_correct(self, answer_id): - if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct' + if answer_id in self.cmap: + return self.cmap[answer_id]['correctness'] == 'correct' return None def is_queued(self, answer_id): @@ -94,14 +112,18 @@ class CorrectMap(object): return npoints elif self.is_correct(answer_id): return 1 - return 0 # if not correct and no points have been assigned, return 0 + # if not correct and no points have been assigned, return 0 + return 0 def set_property(self, answer_id, property, value): - if answer_id in self.cmap: self.cmap[answer_id][property] = value - else: self.cmap[answer_id] = {property: value} + if answer_id in self.cmap: + self.cmap[answer_id][property] = value + else: + self.cmap[answer_id] = {property: value} def get_property(self, answer_id, property, default=None): - if answer_id in self.cmap: return self.cmap[answer_id].get(property, default) + if answer_id in self.cmap: + return self.cmap[answer_id].get(property, default) return default def get_correctness(self, answer_id): diff --git a/common/lib/capa/capa/eia.py b/common/lib/capa/capa/eia.py index b41f205576..f781d46a3f 100644 --- a/common/lib/capa/capa/eia.py +++ b/common/lib/capa/capa/eia.py @@ -1,9 +1,15 @@ -""" Standard resistor codes. +""" +Standard resistor codes. http://en.wikipedia.org/wiki/Electronic_color_code """ E6 = [10, 15, 22, 33, 47, 68] + E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82] + E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91] + E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953] + E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976] + E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988] diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 9ae63fb43a..0e993c1366 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -2,7 +2,7 @@ # File: courseware/capa/inputtypes.py # -''' +""" Module containing the problem elements which render into input objects - textline @@ -16,15 +16,16 @@ Module containing the problem elements which render into input objects - optioninput (for option list) - filesubmission (upload a file) -These are matched by *.html files templates/*.html which are mako templates with the actual html. +These are matched by *.html files templates/*.html which are mako templates with the +actual html. -Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status' +Each input type takes the xml tree as 'element', the previous answer as 'value', and the +graded status as'status' +""" -''' - -# TODO: rename "state" to "status" for all below -# status is currently the answer for the problem ID for the input element, -# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element. +# TODO: rename "state" to "status" for all below. status is currently the answer for the +# problem ID for the input element, but it will turn into a dict containing both the +# answer and any associated message for the problem ID for the input element. import logging import re @@ -36,113 +37,196 @@ import xml.sax.saxutils as saxutils log = logging.getLogger('mitx.' + __name__) +######################################################################### -def get_input_xml_tags(): - ''' Eventually, this will be for all registered input types ''' - return SimpleInput.get_xml_tags() +_TAGS_TO_CLASSES = {} + +def register_input_class(cls): + """ + Register cls as a supported input type. It is expected to have the same constructor as + InputTypeBase, and to define cls.tags as a list of tags that it implements. + + If an already-registered input type has claimed one of those tags, will raise ValueError. + + If there are no tags in cls.tags, will also raise ValueError. + """ + + # Do all checks and complain before changing any state. + if len(cls.tags) == 0: + raise ValueError("No supported tags for class {0}".format(cls.__name__)) + + for t in cls.tags: + if t in _TAGS_TO_CLASSES: + other_cls = _TAGS_TO_CLASSES[t] + if cls == other_cls: + # registering the same class multiple times seems silly, but ok + continue + raise ValueError("Tag {0} already registered by class {1}. Can't register for class {2}" + .format(t, other_cls.__name__, cls.__name__)) + + # Ok, should be good to change state now. + for t in cls.tags: + _TAGS_TO_CLASSES[t] = cls + +def registered_input_tags(): + """ + Get a list of all the xml tags that map to known input types. + """ + return _TAGS_TO_CLASSES.keys() -class SimpleInput():# XModule - ''' - Type for simple inputs -- plain HTML with a form element - ''' +def get_class_for_tag(tag): + """ + For any tag in registered_input_tags(), return the corresponding class. Otherwise, will raise KeyError. + """ + return _TAGS_TO_CLASSES[tag] - xml_tags = {} # # Maps tags to functions - def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'): - ''' - Instantiate a SimpleInput class. Arguments: +class InputTypeBase(object): + """ + Abstract base class for input types. + """ - - system : ModuleSystem instance which provides OS, rendering, and user context + template = None + + def __init__(self, system, xml, state): + """ + Instantiate an InputType class. Arguments: + + - system : ModuleSystem instance which provides OS, rendering, and user context. Specifically, must + have a render_template function. - xml : Element tree of this Input element - - item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string - - track_url : URL used for tracking - string - state : a dictionary with optional keys: - * Value - * ID - * Status (answered, unanswered, unsubmitted) - * Feedback (dictionary containing keys for hints, errors, or other - feedback from previous attempt) - - use : - ''' + * 'value' + * 'id' + * 'status' (answered, unanswered, unsubmitted) + * 'feedback' (dictionary containing keys for hints, errors, or other + feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode' + is 'always', the hint is always displayed.) + """ self.xml = xml self.tag = xml.tag self.system = system - if not state: state = {} - ## ID should only come from one place. - ## If it comes from multiple, we use state first, XML second, and parameter - ## third. Since we don't make this guarantee, we can swap this around in - ## the future if there's a more logical order. - if item_id: self.id = item_id - if xml.get('id'): self.id = xml.get('id') - if 'id' in state: self.id = state['id'] + ## NOTE: ID should only come from one place. If it comes from multiple, + ## we use state first, XML second (in case the xml changed, but we have + ## existing state with an old id). Since we don't make this guarantee, + ## we can swap this around in the future if there's a more logical + ## order. - self.value = '' - if 'value' in state: - self.value = state['value'] + self.id = state.get('id', xml.get('id')) + if self.id is None: + raise ValueError("input id state is None. xml is {0}".format(etree.tostring(xml))) - self.msg = '' - feedback = state.get('feedback') - if feedback is not None: - self.msg = feedback.get('message', '') - self.hint = feedback.get('hint', '') - self.hintmode = feedback.get('hintmode', None) + self.value = state.get('value', '') - # put hint above msg if to be displayed - if self.hintmode == 'always': - self.msg = self.hint + ('
' if self.msg else '') + self.msg + feedback = state.get('feedback', {}) + self.msg = feedback.get('message', '') + self.hint = feedback.get('hint', '') + self.hintmode = feedback.get('hintmode', None) - self.status = 'unanswered' - if 'status' in state: - self.status = state['status'] + # put hint above msg if it should be displayed + if self.hintmode == 'always': + self.msg = self.hint + ('
' if self.msg else '') + self.msg - @classmethod - def get_xml_tags(c): - return c.xml_tags.keys() + self.status = state.get('status', 'unanswered') - @classmethod - def get_uses(c): - return ['capa_input', 'capa_transform'] + def _get_render_context(self): + """ + Abstract method. Subclasses should implement to return the dictionary + of keys needed to render their template. - def get_html(self): - return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg) - - -def register_render_function(fn, names=None, cls=SimpleInput): - if names is None: - SimpleInput.xml_tags[fn.__name__] = fn - else: + (Separate from get_html to faciliate testing of logic separately from the rendering) + """ raise NotImplementedError - def wrapped(): - return fn - return wrapped + def get_html(self): + """ + Return the html for this input, as an etree element. + """ + if self.template is None: + raise NotImplementedError("no rendering template specified for class {0}".format(self.__class__)) + + html = self.system.render_template(self.template, self._get_render_context()) + return etree.XML(html) + + +## TODO: Remove once refactor is complete +def make_class_for_render_function(fn): + """ + Take an old-style render function, return a new-style input class. + """ + + class Impl(InputTypeBase): + """ + Inherit all the constructor logic from InputTypeBase... + """ + tags = [fn.__name__] + def get_html(self): + """...delegate to the render function to do the work""" + return fn(self.xml, self.value, self.status, self.system.render_template, self.msg) + + # don't want all the classes to be called Impl (confuses register_input_class). + Impl.__name__ = fn.__name__.capitalize() + return Impl + + +def _reg(fn): + """ + Register an old-style inputtype render function as a new-style subclass of InputTypeBase. + This will go away once converting all input types to the new format is complete. (TODO) + """ + register_input_class(make_class_for_render_function(fn)) + #----------------------------------------------------------------------------- -@register_render_function +class OptionInput(InputTypeBase): + """ + Input type for selecting and Select option input type. + + Example: + + The location of the sky + """ + + template = "optioninput.html" + tags = ['optioninput'] + + def _get_render_context(self): + return _optioninput(self.xml, self.value, self.status, self.system.render_template, self.msg) + + def optioninput(element, value, status, render_template, msg=''): - ''' + context = _optioninput(element, value, status, render_template, msg) + html = render_template("optioninput.html", context) + return etree.XML(html) + +def _optioninput(element, value, status, render_template, msg=''): + """ Select option input type. Example: The location of the sky - ''' + """ eid = element.get('id') options = element.get('options') if not options: - raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)) + raise Exception( + "[courseware.capa.inputtypes.optioninput] Missing options specification in " + + etree.tostring(element)) + + # parse the set of possible options oset = shlex.shlex(options[1:-1]) oset.quotes = "'" oset.whitespace = "," oset = [x[1:-1] for x in list(oset)] - # osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs - osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same + # make ordered list with (key, value) same + osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # TODO: allow ordering to be randomized context = {'id': eid, @@ -152,43 +236,53 @@ def optioninput(element, value, status, render_template, msg=''): 'options': osetdict, 'inline': element.get('inline',''), } + return context - html = render_template("optioninput.html", context) - return etree.XML(html) +register_input_class(OptionInput) #----------------------------------------------------------------------------- # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. -@register_render_function +# @register_render_function def choicegroup(element, value, status, render_template, msg=''): ''' Radio button inputs: multiple choice or true/false - TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute, - ie random, top, bottom. + TODO: allow order of choices to be randomized, following lon-capa spec. Use + "location" attribute, ie random, top, bottom. ''' eid = element.get('id') if element.get('type') == "MultipleChoice": - type = "radio" + element_type = "radio" elif element.get('type') == "TrueFalse": - type = "checkbox" + element_type = "checkbox" else: - type = "radio" + element_type = "radio" choices = [] for choice in element: if not choice.tag == 'choice': - raise Exception("[courseware.capa.inputtypes.choicegroup] Error only tags should be immediate children of a , found %s instead" % choice.tag) + raise Exception("[courseware.capa.inputtypes.choicegroup] " + "Error: only tags should be immediate children " + "of a , found %s instead" % choice.tag) ctext = "" - ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it? + # TODO: what if choice[0] has math tags in it? + ctext += ''.join([etree.tostring(x) for x in choice]) if choice.text is not None: - ctext += choice.text # TODO: fix order? + # TODO: fix order? + ctext += choice.text choices.append((choice.get("name"), ctext)) - context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'name_array_suffix': ''} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': element_type, + 'choices': choices, + 'name_array_suffix': ''} html = render_template("choicegroup.html", context) return etree.XML(html) +_reg(choicegroup) #----------------------------------------------------------------------------- def extract_choices(element): @@ -196,8 +290,8 @@ def extract_choices(element): Extracts choices for a few input types, such as radiogroup and checkboxgroup. - TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute, - ie random, top, bottom. + TODO: allow order of choices to be randomized, following lon-capa spec. Use + "location" attribute, ie random, top, bottom. ''' choices = [] @@ -216,7 +310,6 @@ def extract_choices(element): # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. -@register_render_function def radiogroup(element, value, status, render_template, msg=''): ''' Radio button inputs: (multiple choice) @@ -226,15 +319,21 @@ def radiogroup(element, value, status, render_template, msg=''): choices = extract_choices(element) - context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'name_array_suffix': '[]'} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': 'radio', + 'choices': choices, + 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) +_reg(radiogroup) + # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. -@register_render_function def checkboxgroup(element, value, status, render_template, msg=''): ''' Checkbox inputs: (select one or more choices) @@ -244,12 +343,18 @@ def checkboxgroup(element, value, status, render_template, msg=''): choices = extract_choices(element) - context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'name_array_suffix': '[]'} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': 'checkbox', + 'choices': choices, + 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) -@register_render_function +_reg(checkboxgroup) + def javascriptinput(element, value, status, render_template, msg='null'): ''' Hidden field for javascript to communicate via; also loads the required @@ -260,60 +365,80 @@ def javascriptinput(element, value, status, render_template, msg='null'): problem_state = element.get('problem_state') display_class = element.get('display_class') display_file = element.get('display_file') - + # Need to provide a value that JSON can parse if there is no # student-supplied value yet. if value == "": value = 'null' - + escapedict = {'"': '"'} value = saxutils.escape(value, escapedict) msg = saxutils.escape(msg, escapedict) - context = {'id': eid, 'params': params, 'display_file': display_file, - 'display_class': display_class, 'problem_state': problem_state, - 'value': value, 'evaluation': msg, + context = {'id': eid, + 'params': params, + 'display_file': display_file, + 'display_class': display_class, + 'problem_state': problem_state, + 'value': value, + 'evaluation': msg, } html = render_template("javascriptinput.html", context) return etree.XML(html) +_reg(javascriptinput) -@register_render_function def textline(element, value, status, render_template, msg=""): ''' Simple text line input, with optional size specification. ''' - if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x - return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg) + # TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x + if element.get('math') or element.get('dojs'): + return textline_dynamath(element, value, status, render_template, msg) eid = element.get('id') if eid is None: msg = 'textline has no id: it probably appears outside of a known response type' msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '') raise Exception(msg) + count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') + + # Escape answers with quotes, so they don't crash the system! escapedict = {'"': '"'} - value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system! - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden, + value = saxutils.escape(value, escapedict) + + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'hidden': hidden, 'inline': element.get('inline',''), } + html = render_template("textinput.html", context) try: xhtml = etree.XML(html) except Exception as err: - if True: # TODO needs to be self.system.DEBUG - but can't access system + # TODO: needs to be self.system.DEBUG - but can't access system + if True: log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html) raise return xhtml +_reg(textline) + #----------------------------------------------------------------------------- -@register_render_function def textline_dynamath(element, value, status, render_template, msg=''): ''' - Text line input with dynamic math display (equation rendered on client in real time during input). + Text line input with dynamic math display (equation rendered on client in real time + during input). ''' # TODO: Make a wrapper for # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types @@ -325,7 +450,8 @@ def textline_dynamath(element, value, status, render_template, msg=''): eid = element.get('id') count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') # Preprocessor to insert between raw input and Mathjax preprocessor = {'class_name': element.get('preprocessorClassName',''), @@ -337,16 +463,19 @@ def textline_dynamath(element, value, status, render_template, msg=''): escapedict = {'"': '"'} value = saxutils.escape(value, escapedict) - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, - 'msg': msg, 'hidden': hidden, - 'preprocessor': preprocessor, - } + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'hidden': hidden, + 'preprocessor': preprocessor,} html = render_template("textinput_dynamath.html", context) return etree.XML(html) #----------------------------------------------------------------------------- -@register_render_function def filesubmission(element, value, status, render_template, msg=''): ''' Upload a single file (e.g. for programming assignments) @@ -360,22 +489,27 @@ def filesubmission(element, value, status, render_template, msg=''): # Check if problem has been queued queue_len = 0 - if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of queue + if status == 'incomplete': status = 'queued' queue_len = msg msg = 'Submitted to grader.' - context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, - 'queue_len': queue_len, 'allowed_files': allowed_files, - 'required_files': required_files - } + context = { 'id': eid, + 'state': status, + 'msg': msg, + 'value': value, + 'queue_len': queue_len, + 'allowed_files': allowed_files, + 'required_files': required_files,} html = render_template("filesubmission.html", context) return etree.XML(html) +_reg(filesubmission) + #----------------------------------------------------------------------------- ## TODO: Make a wrapper for -@register_render_function def textbox(element, value, status, render_template, msg=''): ''' The textbox is used for code input. The message is the return HTML string from @@ -387,13 +521,17 @@ def textbox(element, value, status, render_template, msg=''): size = element.get('size') rows = element.get('rows') or '30' cols = element.get('cols') or '80' - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') - if not value: value = element.text # if no student input yet, then use the default input given by the problem + # if no student input yet, then use the default input given by the problem + if not value: + value = element.text # Check if problem has been queued queue_len = 0 - if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of queue + if status == 'incomplete': status = 'queued' queue_len = msg msg = 'Submitted to grader.' @@ -404,10 +542,18 @@ def textbox(element, value, status, render_template, msg=''): tabsize = element.get('tabsize','4') tabsize = int(tabsize) - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, - 'mode': mode, 'linenumbers': linenumbers, - 'rows': rows, 'cols': cols, - 'hidden': hidden, 'tabsize': tabsize, + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'mode': mode, + 'linenumbers': linenumbers, + 'rows': rows, + 'cols': cols, + 'hidden': hidden, + 'tabsize': tabsize, 'queue_len': queue_len, } html = render_template("textbox.html", context) @@ -422,8 +568,9 @@ def textbox(element, value, status, render_template, msg=''): return xhtml +_reg(textbox) + #----------------------------------------------------------------------------- -@register_render_function def schematic(element, value, status, render_template, msg=''): eid = element.get('id') height = element.get('height') @@ -446,10 +593,10 @@ def schematic(element, value, status, render_template, msg=''): html = render_template("schematicinput.html", context) return etree.XML(html) +_reg(schematic) #----------------------------------------------------------------------------- ### TODO: Move out of inputtypes -@register_render_function def math(element, value, status, render_template, msg=''): ''' This is not really an input type. It is a convention from Lon-CAPA, used for @@ -475,7 +622,8 @@ def math(element, value, status, render_template, msg=''): # mathstr = mathstr.replace('\\displaystyle','') #else: # isinline = True - # html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail}) + # html = render_template("mathstring.html", {'mathstr':mathstr, + # 'isinline':isinline,'tail':element.tail}) html = '%s%s' % (mathstr, saxutils.escape(element.tail)) try: @@ -483,25 +631,27 @@ def math(element, value, status, render_template, msg=''): except Exception as err: if False: # TODO needs to be self.system.DEBUG - but can't access system msg = '

Error %s

' % str(err).replace('<', '<') - msg += '

Failed to construct math expression from

%s

' % html.replace('<', '<') + msg += ('

Failed to construct math expression from

%s

' % + html.replace('<', '<')) msg += "
" log.error(msg) return etree.XML(msg) else: raise - # xhtml.tail = element.tail # don't forget to include the tail! + # xhtml.tail = element.tail # don't forget to include the tail! return xhtml +_reg(math) + #----------------------------------------------------------------------------- -@register_render_function def solution(element, value, status, render_template, msg=''): ''' This is not really an input type. It is just a ... which is given an ID, that is used for displaying an extended answer (a problem "solution") after "show answers" is pressed. Note that the solution content is NOT sent with the HTML. It is obtained - by a JSON call. + by an ajax call. ''' eid = element.get('id') size = element.get('size') @@ -514,17 +664,20 @@ def solution(element, value, status, render_template, msg=''): html = render_template("solutionspan.html", context) return etree.XML(html) +_reg(solution) + #----------------------------------------------------------------------------- -@register_render_function def imageinput(element, value, status, render_template, msg=''): ''' - Clickable image as an input field. Element should specify the image source, height, and width, eg - + Clickable image as an input field. Element should specify the image source, height, + and width, e.g. - TODO: showanswer for imageimput does not work yet - need javascript to put rectangle over acceptable area of image. + + TODO: showanswer for imageimput does not work yet - need javascript to put rectangle + over acceptable area of image. ''' eid = element.get('id') src = element.get('src') @@ -551,3 +704,80 @@ def imageinput(element, value, status, render_template, msg=''): } html = render_template("imageinput.html", context) return etree.XML(html) + +_reg(imageinput) + +#----------------------------------------------------------------------------- +def crystallography(element, value, status, render_template, msg=''): + eid = element.get('id') + if eid is None: + msg = 'cryst has no id: it probably appears outside of a known response type' + msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '') + raise Exception(msg) + height = element.get('height') + width = element.get('width') + display_file = element.get('display_file') + + count = int(eid.split('_')[-2]) - 1 # HACK + size = element.get('size') + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') + # Escape answers with quotes, so they don't crash the system! + escapedict = {'"': '"'} + value = saxutils.escape(value, escapedict) + + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'hidden': hidden, + 'inline': element.get('inline', ''), + 'width': width, + 'height': height, + 'display_file': display_file, + } + + html = render_template("crystallography.html", context) + try: + xhtml = etree.XML(html) + except Exception as err: + # TODO: needs to be self.system.DEBUG - but can't access system + if True: + log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html) + raise + return xhtml + +_reg(crystallography) + + +#-------------------------------------------------------------------------------- + + +class ChemicalEquationInput(InputTypeBase): + """ + An input type for entering chemical equations. Supports live preview. + + Example: + + + + options: size -- width of the textbox. + """ + + template = "chemicalequationinput.html" + tags = ['chemicalequationinput'] + + def _get_render_context(self): + size = self.xml.get('size', '20') + context = { + 'id': self.id, + 'value': self.value, + 'status': self.status, + 'size': size, + 'previewer': '/static/js/capa/chemical_equation_preview.js', + } + return context + +register_input_class(ChemicalEquationInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d97741613c..46b683cf3c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -77,18 +77,27 @@ class LoncapaResponse(object): In addition, these methods are optional: - - setup_response : find and note the answer input field IDs for the response; called by __init__ - - check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed - - render_html : render this Response as HTML (must return XHTML compliant string) + - setup_response : find and note the answer input field IDs for the response; called + by __init__ + + - check_hint_condition : check to see if the student's answers satisfy a particular + condition for a hint to be displayed + + - render_html : render this Response as HTML (must return XHTML-compliant string) - __unicode__ : unicode representation of this Response Each response type may also specify the following attributes: - - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) - - allowed_inputfields : list of allowed input fields (each a string) for this Response - - required_attributes : list of required attributes (each a string) on the main response XML stanza - - hint_tag : xhtml tag identifying hint associated with this response inside hintgroup + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ + if not None) + - allowed_inputfields : list of allowed input fields (each a string) for this Response + + - required_attributes : list of required attributes (each a string) on the main + response XML stanza + + - hint_tag : xhtml tag identifying hint associated with this response inside + hintgroup """ __metaclass__ = abc.ABCMeta # abc = Abstract Base Class @@ -121,26 +130,32 @@ class LoncapaResponse(object): raise LoncapaProblemError(msg) if self.max_inputfields and len(inputfields) > self.max_inputfields: - msg = "%s: cannot have more than %s input fields" % (unicode(self), self.max_inputfields) + msg = "%s: cannot have more than %s input fields" % ( + unicode(self), self.max_inputfields) msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '') raise LoncapaProblemError(msg) for prop in self.required_attributes: if not xml.get(prop): - msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self), prop) + msg = "Error in problem specification: %s missing required attribute %s" % ( + unicode(self), prop) msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '') raise LoncapaProblemError(msg) - self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response + # ordered list of answer_id values for this response + self.answer_ids = [x.get('id') for x in self.inputfields] if self.max_inputfields == 1: - self.answer_id = self.answer_ids[0] # for convenience + # for convenience + self.answer_id = self.answer_ids[0] self.maxpoints = dict() for inputfield in self.inputfields: - maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point + # By default, each answerfield is worth 1 point + maxpoints = inputfield.get('points', '1') self.maxpoints.update({inputfield.get('id'): int(maxpoints)}) - self.default_answer_map = {} # dict for default answer map (provided in input elements) + # dict for default answer map (provided in input elements) + self.default_answer_map = {} for entry in self.inputfields: answer = entry.get('correct_answer') if answer: @@ -163,12 +178,18 @@ class LoncapaResponse(object): - renderer : procedure which produces HTML given an ElementTree ''' - tree = etree.Element('span') # render ourself as a + our content - if self.xml.get('inline',''): # problem author can make this span display:inline + # render ourself as a + our content + tree = etree.Element('span') + + # problem author can make this span display:inline + if self.xml.get('inline',''): tree.set('class','inline') + for item in self.xml: - item_xhtml = renderer(item) # call provided procedure to do the rendering - if item_xhtml is not None: tree.append(item_xhtml) + # call provided procedure to do the rendering + item_xhtml = renderer(item) + if item_xhtml is not None: + tree.append(item_xhtml) tree.tail = self.xml.tail return tree @@ -194,21 +215,21 @@ class LoncapaResponse(object): Modifies new_cmap, by adding hints to answer_id entries as appropriate. ''' hintgroup = self.xml.find('hintgroup') - if hintgroup is None: return + if hintgroup is None: + return # hint specified by function? hintfn = hintgroup.get('hintfn') if hintfn: ''' - Hint is determined by a function defined in the ''' - snippets = [{'snippet': ''' + snippets = [{'snippet': """
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) @@ -804,8 +854,8 @@ class CustomResponse(LoncapaResponse): if not(r=="IS*u(t-t0)"): correct[0] ='incorrect' -
'''}, - {'snippet': ''' stanza instead + # if we have a "cfn" attribute then look for the function specified by cfn, in + # the problem context ie the comparison function is defined in the + # stanza instead cfn = xml.get('cfn') if cfn: log.debug("cfn = %s" % cfn) @@ -849,13 +901,14 @@ def sympy_check2(): self.code = self.context[cfn] else: msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', + '') raise LoncapaProblemError(msg) if not self.code: if answer is None: - # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid - log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) + log.error("[courseware.capa.responsetypes.customresponse] missing" + " code checking script! id=%s" % self.myid) self.code = '' else: answer_src = answer.get('src') @@ -872,43 +925,70 @@ def sympy_check2(): log.debug('%s: student_answers=%s' % (unicode(self), student_answers)) - idset = sorted(self.answer_ids) # ordered list of answer id's + # ordered list of answer id's + idset = sorted(self.answer_ids) try: - submission = [student_answers[k] for k in idset] # ordered list of answers + # ordered list of answers + submission = [student_answers[k] for k in idset] except Exception as err: - msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers + msg = ('[courseware.capa.responsetypes.customresponse] error getting' + ' student answer from %s' % student_answers) msg += '\n idset = %s, error = %s' % (idset, err) log.error(msg) raise Exception(msg) # global variable in context which holds the Presentation MathML from dynamic math input - dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses + # ordered list of dynamath responses + dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # if there is only one box, and it's empty, then don't evaluate if len(idset) == 1 and not submission[0]: - # default to no error message on empty answer (to be consistent with other responsetypes) - # but allow author to still have the old behavior by setting empty_answer_err attribute - msg = 'No answer entered!' if self.xml.get('empty_answer_err') else '' + # default to no error message on empty answer (to be consistent with other + # responsetypes) but allow author to still have the old behavior by setting + # empty_answer_err attribute + msg = ('No answer entered!' + if self.xml.get('empty_answer_err') else '') return CorrectMap(idset[0], 'incorrect', msg=msg) - # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's + # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are + # not expecting 'unknown's correct = ['unknown'] * len(idset) messages = [''] * len(idset) # put these in the context of the check function evaluator # note that this doesn't help the "cfn" version - only the exec version - self.context.update({'xml': self.xml, # our subtree - 'response_id': self.myid, # my ID - 'expect': self.expect, # expected answer (if given as attribute) - 'submission': submission, # ordered list of student answers from entry boxes in our subtree - 'idset': idset, # ordered list of ID's of all entry boxes in our subtree - 'dynamath': dynamath, # ordered list of all javascript inputs in our subtree - 'answers': student_answers, # dict of student's responses, with keys being entry box IDs - 'correct': correct, # the list to be filled in by the check function - 'messages': messages, # the list of messages to be filled in by the check function - 'options': self.xml.get('options'), # any options to be passed to the cfn - 'testdat': 'hello world', - }) + self.context.update({ + # our subtree + 'xml': self.xml, + + # my ID + 'response_id': self.myid, + + # expected answer (if given as attribute) + 'expect': self.expect, + + # ordered list of student answers from entry boxes in our subtree + 'submission': submission, + + # ordered list of ID's of all entry boxes in our subtree + 'idset': idset, + + # ordered list of all javascript inputs in our subtree + 'dynamath': dynamath, + + # dict of student's responses, with keys being entry box IDs + 'answers': student_answers, + + # the list to be filled in by the check function + 'correct': correct, + + # the list of messages to be filled in by the check function + 'messages': messages, + + # any options to be passed to the cfn + 'options': self.xml.get('options'), + 'testdat': 'hello world', + }) # pass self.system.debug to cfn self.context['debug'] = self.system.DEBUG @@ -923,8 +1003,10 @@ def sympy_check2(): print "oops in customresponse (code) error %s" % err print "context = ", self.context print traceback.format_exc() - raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student - else: # self.code is not a string; assume its a function + # Notify student + raise StudentInputError("Error: Problem could not be evaluated with your input") + else: + # self.code is not a string; assume its a function # this is an interface to the Tutor2 check functions fn = self.code @@ -960,7 +1042,8 @@ def sympy_check2(): msg = '' + msg + '' msg = msg.replace('<', '<') #msg = msg.replace('<','<') - msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), + pretty_print=True) #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) msg = msg.replace(' ', '') #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 @@ -1024,18 +1107,19 @@ class SymbolicResponse(CustomResponse): class CodeResponse(LoncapaResponse): - ''' + """ Grade student code using an external queueing server, called 'xqueue' Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL where results are posted (string), + 'callback_url': Per-StudentModule callback URL + where results are posted (string), 'default_queuename': Default queuename to submit request (string) } - External requests are only submitted for student submission grading + External requests are only submitted for student submission grading (i.e. and not for getting reference answers) - ''' + """ response_tag = 'coderesponse' allowed_inputfields = ['textbox', 'filesubmission'] @@ -1048,7 +1132,8 @@ class CodeResponse(LoncapaResponse): TODO: Determines whether in synchronous or asynchronous (queued) mode ''' xml = self.xml - self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL + # TODO: XML can override external resource (grader/queue) URL + self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) # VS[compat]: @@ -1109,7 +1194,8 @@ class CodeResponse(LoncapaResponse): # Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is: # (1) Internal edX code, i.e. NOT student submissions, and - # (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program' + # (2) The code should only define the strings 'initial_display', 'answer', + # 'preamble', 'test_program' # following the ExternalResponse XML format penv = {} penv['__builtins__'] = globals()['__builtins__'] @@ -1122,10 +1208,12 @@ class CodeResponse(LoncapaResponse): self.answer = penv['answer'] self.initial_display = penv['initial_display'] except Exception as err: - log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in ..." % err) + log.error("Error in CodeResponse %s: Problem reference code does not define" + " 'answer' and/or 'initial_display' in ..." % err) raise Exception(err) - # Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface + # Finally, make the ExternalResponse input XML format conform to the generic + # exteral grader interface # The XML tagging of grader_payload is pyxserver-specific grader_payload = '' grader_payload += '' + tests + '\n' @@ -1135,14 +1223,16 @@ class CodeResponse(LoncapaResponse): def get_score(self, student_answers): try: - submission = student_answers[self.answer_id] # Note that submission can be a file + # Note that submission can be a file + submission = student_answers[self.answer_id] except Exception as err: - log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' % + log.error('Error in CodeResponse %s: cannot get student answer for %s;' + ' student_answers=%s' % (err, self.answer_id, convert_files_to_filenames(student_answers))) raise Exception(err) # Prepare xqueue request - #------------------------------------------------------------ + #------------------------------------------------------------ qinterface = self.system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -1151,19 +1241,20 @@ class CodeResponse(LoncapaResponse): # Generate header queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + + anonymous_student_id + self.answer_id) xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) - + # Generate body if is_list_of_files(submission): - self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue + # TODO: Get S3 pointer from the Queue + self.context.update({'submission': ''}) else: self.context.update({'submission': submission}) - contents = self.payload.copy() + contents = self.payload.copy() # Metadata related to the student submission revealed to the external grader student_info = {'anonymous_student_id': anonymous_student_id, @@ -1173,7 +1264,8 @@ class CodeResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue if is_list_of_files(submission): - contents.update({'student_response': ''}) # TODO: Is there any information we want to send here? + # TODO: Is there any information we want to send here? + contents.update({'student_response': ''}) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents), files_to_upload=submission) @@ -1184,44 +1276,51 @@ class CodeResponse(LoncapaResponse): # State associated with the queueing request queuestate = {'key': queuekey, - 'time': qtime, - } + 'time': qtime,} - cmap = CorrectMap() + cmap = CorrectMap() if error: cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) + msg='Unable to deliver your submission to grader. (Reason: %s.)' + ' Please try again later.' % msg) else: # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox - # and .filesubmission to inform the browser to poll the LMS + # 1) Backend: Non-null CorrectMap['queuestate'] indicates that + # the problem has been queued + # 2) Frontend: correctness='incomplete' eventually trickles down + # through inputtypes.textbox and .filesubmission to inform the + # browser to poll the LMS cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) return cmap def update_score(self, score_msg, oldcmap, queuekey): - (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) + (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: - oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.') + oldcmap.set(self.answer_id, + msg='Invalid grader reply. Please contact the course staff.') return oldcmap - + correctness = 'correct' if correct else 'incorrect' - self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any + # TODO: Find out how this is used elsewhere, if any + self.context['correct'] = correctness - # Replace 'oldcmap' with new grading results if queuekey matches. - # If queuekey does not match, we keep waiting for the score_msg whose key actually matches + # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # does not match, we keep waiting for the score_msg whose key actually matches if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points + # Sanity check on returned points if points < 0: points = 0 elif points > self.maxpoints[self.answer_id]: points = self.maxpoints[self.answer_id] - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed + # Queuestate is consumed + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, + msg=msg.replace(' ', ' '), queuestate=None) else: - log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) + log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % + (queuekey, self.answer_id)) return oldcmap @@ -1233,7 +1332,7 @@ class CodeResponse(LoncapaResponse): return {self.answer_id: self.initial_display} def _parse_score_msg(self, score_msg): - ''' + """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, 'score': Numeric value (floating point is okay) to assign to answer @@ -1244,22 +1343,25 @@ class CodeResponse(LoncapaResponse): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) msg: Message from grader to display to student (string) - ''' + """ fail = (False, False, 0, '') try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg) + log.error("External grader message should be a JSON-serialized dict." + " Received score_msg = %s" % score_msg) return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result) + log.error("External grader message should be a JSON-serialized dict." + " Received score_result = %s" % score_result) return fail for tag in ['correct', 'score', 'msg']: if tag not in score_result: - log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'") + log.error("External grader message is missing one or more required" + " tags: 'correct', 'score', 'msg'") return fail - # Next, we need to check that the contents of the external grader message + # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? @@ -1267,11 +1369,12 @@ class CodeResponse(LoncapaResponse): try: etree.fromstring(msg) except etree.XMLSyntaxError as err: - log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg) + log.error("Unable to parse external grader message as valid" + " XML: score_msg['msg']=%s" % msg) return fail - + return (True, score_result['correct'], score_result['score'], msg) - + #----------------------------------------------------------------------------- @@ -1327,9 +1430,9 @@ main() def setup_response(self): xml = self.xml - self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + # FIXME - hardcoded URL + self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" - # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer = xml.find('answer') if answer is not None: answer_src = answer.get('src') @@ -1337,7 +1440,8 @@ main() self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text - else: # no stanza; get code from @@ -48,10 +92,17 @@ @@ -121,13 +172,11 @@ - - + + + + + diff --git a/lms/templates/email_change_successful.html b/lms/templates/email_change_successful.html index ed3b732d6e..0b05a28bf5 100644 --- a/lms/templates/email_change_successful.html +++ b/lms/templates/email_change_successful.html @@ -1,3 +1,3 @@

E-mail change successful!

-

You should see your new name in your profile. +

You should see your new email in your dashboard.

\ No newline at end of file diff --git a/lms/templates/emails/confirm_email_change.txt b/lms/templates/emails/confirm_email_change.txt index e1b5d63376..02aa20facf 100644 --- a/lms/templates/emails/confirm_email_change.txt +++ b/lms/templates/emails/confirm_email_change.txt @@ -1,8 +1,7 @@ <%! from django.core.urlresolvers import reverse %> This is to confirm that you changed the e-mail associated with edX from ${old_email} to ${new_email}. If you did not make this request, -please contact the course staff immediately. Contact information is -listed at: +please contact us immediately. Contact information is listed at: % if is_secure: https://${ site }${reverse('contact')} diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index 68a9f11965..0515515ba2 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -6,16 +6,25 @@ ## EdX Blog - 2012-07-16T14:08:12-07:00 + 2012-10-14T14:08:12-07:00 - tag:www.edx.org,2012:Post/5 - 2012-09-25T14:00:00-07:00 - 2012-09-25T14:00:00-07:00 - - Elsevier collaborates with edX - <img src="${static.url('images/press/foundations-of-analog-109x84.jpg')}" /> - <p>Free course textbook made available to edX students</p> + tag:www.edx.org,2012:Post/6 + 2012-10-15T14:00:00-07:00 + 2012-10-14T14:00:00-07:00 + + The University of Texas System joins edX + <img src="${static.url('images/press/uts-seal_109x84.jpg')}" /> + <p>Nine universities and six health institutions</p> + + + + + + + + + tag:www.edx.org,2012:Post/4 2012-09-06T14:00:00-07:00 diff --git a/lms/templates/index.html b/lms/templates/index.html index fc6f1f336b..151525f715 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -65,7 +65,7 @@ -
  • +
  • @@ -73,6 +73,14 @@
  • +
  • + + +
    + UTx +
    +
    +
  • diff --git a/lms/templates/licenses/serial_numbers.html b/lms/templates/licenses/serial_numbers.html new file mode 100644 index 0000000000..18f0ff8a9b --- /dev/null +++ b/lms/templates/licenses/serial_numbers.html @@ -0,0 +1,10 @@ +
    +% for license in licenses: +
    ${license.software.name}:
    + % if license.serial: +
    ${license.serial}
    + % else: +
    None Available
    + % endif +% endfor +
    diff --git a/lms/templates/main.html b/lms/templates/main.html index dd44ebb1f3..f234aa72cf 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -11,7 +11,6 @@ <%static:js group='main_vendor'/> <%block name="headextra"/> - @@ -20,9 +19,26 @@ <%static:css group='ie-fixes'/> + + % if not course: + + % endif + <%include file="navigation.html" />
    ${self.body()} diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 210fd61ead..d574bc3f6e 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -8,19 +8,41 @@ from django.core.urlresolvers import reverse # App that handles subdomain specific branding import branding +# app that handles site status messages +from status.status import get_site_status_msg %> -%if course: +<%block cached="False"> +<% +try: + course_id = course.id +except: + # can't figure out a better way to get at a possibly-defined course var + course_id = None +site_status_msg = get_site_status_msg(course_id) +%> +% if site_status_msg: +
    +
    + +

    ${site_status_msg}

    +
    +
    +% endif + + + +% if course:
    -%else: +% else:
    -%endif +% endif %endif +
    +% if course: +
    Warning: Your browser is not fully supported. We strongly recommend using Chrome or Firefox.
    +% endif %if not user.is_authenticated(): <%include file="login_modal.html" /> diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index 6d906c84ff..bf6c3e0891 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -5,7 +5,8 @@ {% block reset_link %} https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {% endblock %} -{% trans "Your username, in case you've forgotten:" %} {{ user.username }} + +If you didn't request this change, you can disregard this email - we have not yet reset your password. {% trans "Thanks for using our site!" %} diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index e2f3f9efbe..a8f6268bd5 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -1,107 +1,102 @@ -<%! from django.core.urlresolvers import reverse %> -<%namespace name='static' file='../static_content.html'/> - -<%inherit file="../main.html" /> - -<%block name="title">FAQ - -
    - -
    -
    -
    -

    Organization

    -
    -

    What is edX?

    -

    EdX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

    -

    EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. These institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

    -
    -
    -

    What are "X Universities"?

    -

    Harvard, MIT and UC Berkeley, as the first universities whose courses are delivered on the edX website, are "X Universities." The three institutions will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities" as soon as possible. Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

    -
    -
    -

    Why is UC Berkeley joining edX?

    -

    Like Harvard and MIT, UC Berkeley seeks to transform education in quality, efficiency and scale through technology and research, for the benefit of campus-based students and the global community of online learners.

    -

    UC Berkeley also shares the edX commitment to the not-for-profit and open-platform model as a way to enhance human fulfillment worldwide.

    -
    -
    -

    What will UC Berkeley's direct participation entail?

    -

    UC Berkeley will begin by offering two courses on edX in Fall 2012, and will collaborate on the development of the technology platform. We will explore, experiment and innovate together.

    -

    UC Berkeley will also serve as the inaugural chair of the "X University" Consortium for an initial 5 year period. As Chair, UC Berkeley will participate on the edX Board on behalf of the X Universities.

    -
    -
    -

    Why is edX only adding one X University?

    -

    More than 120 universities from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best non-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning.

    -

    EdX will actively explore the addition of other institutions from around the world to the edX platform, and we look forward to adding more "X Universities" as capacity increases.

    -
    -
    - -
    -

    Students

    -
    -

    Who can take edX courses? Will there be an admissions process?

    -

    EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

    -
    -
    -

    Will certificates be awarded?

    -

    Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

    -
    -
    -

    What will the scope of the online courses be? How many? Which faculty?

    -

    Our goal is to offer a wide variety of courses across disciplines. There are currently seven courses offered for Fall 2012.

    -
    -
    -

    Who is the learner? Domestic or international? Age range?

    -

    Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

    -
    -
    -

    Will participating universities’ standards apply to all courses offered on the edX platform?

    -

    Yes: the reach changes exponentially, but the rigor remains the same.

    -
    -
    -

    How do you intend to test whether this approach is improving learning?

    -

    Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

    -
    -
    -

    How may I apply to study with edX?

    -

    Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

    -
    -
    -

    How may another university participate in edX?

    -

    If you are from a university interested in discussing edX, please email university@edx.org

    -
    -
    - -
    -

    Technology Platform

    -
    -

    What technology will edX use?

    -

    The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

    -

    The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

    -
    -
    -

    How is this different from what other universities are doing online?

    -

    EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

    -
    -
    - -
    - - -
    -
    - -%if user.is_authenticated(): - <%include file="../signup_modal.html" /> -%endif +<%! from django.core.urlresolvers import reverse %> +<%namespace name='static' file='../static_content.html'/> + +<%inherit file="../main.html" /> + +<%block name="title">FAQ + +
    + +
    +
    +
    +

    Organization

    +
    +

    What is edX?

    +

    edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

    +

    EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

    +
    +
    +

    Why is The University of Texas System joining edX?

    +

    Joining edX not only allows UT faculty to showcase their work on a global stage, but also provides UT students the opportunity to take classes from their choice of UT institutions, as well as MIT, Harvard, UC Berkeley and future “X” Universities.

    +

    The UT System closely examined all the alternatives and determined that edX offered the best fit in terms of alignment of mission, platform and revenue model. The strength and reputation of the partner institutions – MIT, Harvard and UC Berkeley – was also a huge consideration. EdX is committed to both blended and online learning and to a non-profit, open source model. It is also governed by a board of academics with a commitment to excellence in learning.

    +
    +
    +

    What will The UT System’s direct participation entail?

    +

    The UT System will begin by offering one course on edX from The University of Texas at Austin in Summer 2013, and four courses in Fall 2013, likely at least one of those courses from one of its health institutions. The UT System is also making a $5 million investment in the edX platform. We will explore, experiment and innovate together.

    +
    +
    +

    Will edX be adding additional X Universities?

    +

    More than 140 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the “X University” Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley and the UT System will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities” as soon as possible. Each member of the consortium will offer courses on the edX platform as an “X University.” The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

    +

    EdX will actively explore the addition of other institutions from around the world to the edX platform, and we look forward to adding more “X Universities” as capacity increases.

    +
    +
    + +
    +

    Students

    +
    +

    Who can take edX courses? Will there be an admissions process?

    +

    EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

    +
    +
    +

    Will certificates be awarded?

    +

    Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

    +
    +
    +

    What will the scope of the online courses be? How many? Which faculty?

    +

    Our goal is to offer a wide variety of courses across disciplines. There are currently seven courses offered for Fall 2012.

    +
    +
    +

    Who is the learner? Domestic or international? Age range?

    +

    Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

    +
    +
    +

    Will participating universities’ standards apply to all courses offered on the edX platform?

    +

    Yes: the reach changes exponentially, but the rigor remains the same.

    +
    +
    +

    How do you intend to test whether this approach is improving learning?

    +

    Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

    +
    +
    +

    How may I apply to study with edX?

    +

    Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

    +
    +
    +

    How may another university participate in edX?

    +

    If you are from a university interested in discussing edX, please email university@edx.org

    +
    +
    + +
    +

    Technology Platform

    +
    +

    What technology will edX use?

    +

    The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

    +

    The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

    +
    +
    +

    How is this different from what other universities are doing online?

    +

    EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

    +
    +
    + +
    + + +
    +
    + +%if user.is_authenticated(): + <%include file="../signup_modal.html" /> +%endif diff --git a/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html b/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html new file mode 100644 index 0000000000..b9d4e95301 --- /dev/null +++ b/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html @@ -0,0 +1,76 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">Cengage Learning To Provide Book Content +
    + + +
    +
    +

    Cengage Learning to Provide Book Content and Pedagogy through edX's Not-for-Profit Interactive Study Via the Web

    +
    + +
    +

    Students taking HarvardX's “PHW207x: Health in Numbers” on edX

    +

    to get access to Principles of Biostatistics, 2nd Edition

    + +

    CAMBRIDGE, MA — October 17, 2012 — EdX, the not-for-profit online learning venture founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced its collaboration with Cengage Learning, a leading educational content, software and services company, to provide licensed content to edX students at no cost.

    + +

    Students who enroll in edX's course PHW207x: Health in Numbers , taught by Professor Marcello Pagano of Harvard's School of Public Health, will have access to an online version of the course textbook, Principles of Biostatistics, 2nd Edition, written by Marcello Pagano and Kimberlee Gauvreau and published by Cengage Learning. Cengage Learning’s instructional design services will also work with edX to migrate the print pedagogy from the textbook into the on-line course, creating the best scope and sequence for effective student learning.

    + +
    + +
    + +

    “edX students worldwide will benefit from both Professor Pagano's in-class lectures and his classic Cengage Learning textbook in biostatics,” said Anant Agarwal, President of edX. “We are very grateful for Cengage's commitment to helping edX learners throughout the world.”

    + +

     “We're pleased to collaborate with edX and its mission to improve worldwide access to higher education,” said William Rieders, Executive Vice President, Global Strategy & Business Development for Cengage Learning. “Through this collaboration, edX and Cengage Learning are able to bring the best content, services, and delivery to students worldwide.”

    + +

    PHW207x: Health in Numbers, which began on October 15th, is the online adaptation of material from the Harvard School of Public Health's classes in epidemiology and biostatistics. Taught by Professor Pagano and Earl Francis Cook, Professor of Epidemiology at the Harvard School of Public Health (HSPH) and at the Harvard Medical School, PHW207x teaches students the principles of biostatistics and epidemiology used for public health and clinical research.

    + +

    Through edX, the “X Universities” — which now includes UC Berkeley and The University of Texas System in addition to founding institutions Harvard and MIT — will provide interactive education wherever there is access to the Internet and will enhance teaching and learning through research about how students learn, and how technologies can facilitate effective teaching both on-campus and online. EdX plans to add other “X Universities” from around the world to the edX platform in the coming months.

    + +

    About edX

    + +

    edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT's Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning—both on-campus and worldwide. EdX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.

    + +

    About Cengage Learning

    + +

    Cengage Learning is a leading provider of innovative teaching, learning and research solutions for the academic, professional and library markets worldwide. The company's products and services are designed to foster academic excellence and professional development, increase student engagement, improve learning outcomes and deliver authoritative information to people whenever and wherever they need it. Through the company's unique position within both the library and academic markets, Cengage Learning is providing integrated learning solutions that bridge from the library to the classroom. Cengage Learning’s brands include Brooks/Cole, Course Technology, Delmar, Gale, Heinle, National Geographic Learning, South-Western and Wadsworth, among others. Cengage Learning is headquartered in Stamford, CT. For more information on Cengage Learning please visit www.cengage.com

    + +
    +

    Contact: Dan O’Connell

    +

    P: 617-480-6585;

    +

    E: oconnell@edx.org

    +
    + + +
    +
    +
    diff --git a/lms/templates/static_templates/press_releases/UT_joins_edX.html b/lms/templates/static_templates/press_releases/UT_joins_edX.html new file mode 100644 index 0000000000..890789efc7 --- /dev/null +++ b/lms/templates/static_templates/press_releases/UT_joins_edX.html @@ -0,0 +1,110 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">The University of Texas System joins edX +
    + + +
    +
    +

    The University of Texas System joins edX

    +
    +
    +

    The University of Texas System joins Harvard, MIT and UC Berkeley in not-for-profit online learning collaborative

    + +

    CAMBRIDGE, MA/AUSTIN, TX – October 15, 2012 — edX, the online non-profit learning initiative founded by Harvard University (Harvard) and the Massachusetts Institute of Technology (MIT) and launched in May, announced today the addition of The University of Texas (UT) System to its platform. The UT System, one of the largest public university systems in the United States with nine academic universities and six health institutions, will collaborate with edX to expand the group of participating “X Universities” – universities offering their courses on the edX platform.

    + +

    The UT System includes the University of Texas at Austin, ranked 25th in the 2012-2013 Times Higher Education World University Rankings, UT Southwestern Medical Center, home to one of the nation's top 25 medical schools, and UT MD Anderson Cancer Center, the nation's No. 1-ranked cancer center. The system's institutions serve 212,000 students and employ 19,000 faculty members.

    + +

    Through edX, the “X Universities” provide online interactive education wherever there is access to the Internet, with a goal to enhance teaching and learning through research about how students learn, and how technologies can facilitate effective teaching both on campus and online. The University of California, Berkeley (UC Berkeley) joined edX in July 2012. edX plans to add other “X Universities” from around the world to the edX platform in the coming months.

    + +

    Francisco G. Cigarroa, Chancellor of The University of Texas System announced the partnership following a unanimous vote of approval by the UT System's Board of Regents on Monday.

    + +

    “New technologies are positively impacting how professors teach and how course content is delivered,” Chancellor Cigarroa said. “The University of Texas System will help lead this revolution and fundamentally alter the direction of online education. We are excited about this partnership with edX and honored to be in the company of such exceptional institutions as MIT, Harvard and Berkeley. The mission of edX aligns perfectly with that of the UT System and keeps the learner as its central focus.”

    + +

    The University of Texas System plans to offer at least four courses on edX within the next year.

    + +

    In addition to serving a global community of online students, the UT System plans to redesign general education courses and traditional entry-level courses that are too often made up of several hundred students. Through its Institute for Transformational Learning, the UT System plans to give students more options by offering courses that are customized to student needs. For example, the UT System plans to offer courses that use a combination of technology and face-to-face interaction, courses that allow students to manage their own time by accelerating through sections they have already mastered or spending more time on areas they find challenging, and fully online courses so students are not limited by their location.

    + +

    “As Texas' flagship university, UT Austin is committed not only to embracing breakthroughs in education, but helping create them,” said William Powers, Jr., President of UT Austin. “We're proud to be partnering with these top peer universities on edX.”

    + +

    As part of a bold and innovative plan, the UT System also plans to offer courses through edX that will allow students to earn college credits toward a degree. “Our goal through our partnership with edX is to better meet the learning needs of a wide range of students, raise graduation rates and cut the cost of higher education, all while maintaining our commitment to education of the highest quality,” said Gene Powell, chairman of the UT System Board of Regents.

    + +

    The UT System brings a large and diverse student body to the edX family. Its six health institutions offer a unique opportunity to provide groundbreaking health and medical courses via edX in the near future. The UT System also brings special expertise in analytics – assessing student learning, online course design and creating interactive learning environments.

    + +

    edX courses are designed to provide students with a wealth of innovative resources, including interactive laboratories, virtual reality environments and access to online tutors and tutorials. Students who take UT System courses through edX won't work in isolation, but will have the opportunity to participate in online forums, network with instructors and fellow students and take part in exciting collaborative projects. “We are excited that The University of Texas System is joining edX's efforts to revolutionize learning,” said Anant Agarwal, President of edX. “The institutions within The University of Texas System bring a wide range of expertise to the edX mission, and with them edX is now positioned to continue to increase our offering of high-quality, online courses.”

    + +

    edX was created by Harvard and MIT in May, with each university committing to contribute $30 million toward the online partnership.

    + +

    “Today's announcement is another important step toward our shared objectives of expanding access to high quality educational content while enhancing teaching and learning online and in the classroom,” said Harvard President Drew Faust. “The addition of The University of Texas System to the edX platform will allow us to deepen our understanding of learning, develop new approaches to teaching that build on that knowledge, and strengthen both the on-campus and online learning experience.” + +

    “At MIT, we are energetically exploring the ways that online instruction can help us reimagine our campus residential education even as it allows us to reach an unprecedented number of learners around the world,” said MIT President L. Rafael Reif. “It is thrilling to be joined by The University of Texas System in the pursuit of that dual goal.”

    + +

    The edX classes to be offered by the UT System will be announced soon and will join other new edX courses planned for Spring, Summer and Fall 2013. As with all edX courses, online learners who obtain a passing grade in the UT System courses will receive a certificate of mastery. edX will also offer the option of proctored examinations for the UT System courses.

    + + +

    About edX

    + +

    edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT's Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning—both on-campus and worldwide. edX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.

    + +

    About Harvard University

    + +

    Harvard University is devoted to excellence in teaching, learning and research, and to developing leaders in many disciplines who make a difference globally. Harvard Faculty are engaged with teaching and research to push the boundaries of human knowledge. The University has twelve degree-granting Schools in addition to the Radcliffe Institute for Advanced Study.

    + +

    Established in 1636, Harvard is the oldest institution of higher education in the United States. The University, which is based in Cambridge and Boston, Massachusetts, has an enrollment of over 20,000 degree candidates, including undergraduate, graduate and professional students. Harvard has more than 360,000 alumni around the world.

    + +

    About MIT

    +

    The Massachusetts Institute of Technology — a coeducational, privately endowed research university founded in 1861 — is dedicated to advancing knowledge and educating students in science, technology and other areas of scholarship that will best serve the nation and the world in the 21st century. The Institute has close to 1,000 faculty and 10,000 undergraduate and graduate students. It is organized into five Schools: Architecture and Urban Planning; Engineering; Humanities, Arts, and Social Sciences; Sloan School of Management; and Science.

    + +

    MIT's commitment to innovation has led to a host of scientific breakthroughs and technological advances. Achievements of the Institute's faculty and graduates have included the first chemical synthesis of penicillin and vitamin A, the development of inertial guidance systems, modern technologies for artificial limbs and the magnetic core memory that made possible the development of digital computers. Seventy-eight alumni, faculty, researchers and staff have won Nobel Prizes.

    + +

    Current areas of research and education include neuroscience and the study of the brain and mind, bioengineering, cancer, energy, the environment and sustainable development, information sciences and technology, new media, financial technology and entrepreneurship.

    + +

    About the University of California, Berkeley

    + +

    The University of California, Berkeley is the world's premier public university with a mission to excel in teaching, research and public service. This longstanding mission has led to the university's distinguished record of Nobel-level scholarship, constant innovation, a concern for the betterment of our world, and consistently high rankings of its schools and departments. The campus offers superior, high value education for extraordinarily talented students from all walks of life; operational excellence and a commitment to the competitiveness and prosperity of California and the nation.

    + +

    The University of California was chartered in 1868 and its flagship campus in Berkeley, on San Francisco Bay, was envisioned as a “City of Learning.” Today, there are more than 1,500 fulltime and 500 part-time faculty members dispersed among more than 130 academic departments and more than 80 interdisciplinary research units. Twenty-two Nobel Prizes have been garnered by faculty and 28 by UC Berkeley alumni. There are 9 Nobel Laureates, 32 MacArthur Fellows, and 4 Pulitzer Prize winners among the current faculty.

    + +

    About The University of Texas System

    + +

    Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state's undergraduate degrees and educates nearly three-fourths of the state's health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state. www.utsystem.edu

    + +
    +

    edX Contact: Dan O’Connell

    +

    oconnell@edx.org

    +

    617-480-6585

    +
    +

    UT System Contact: Jenny LaCoste-Caputo

    +

    jcaputo@utsystem.edu

    +

    512-499-4361

    +
    + + +
    +
    +
    diff --git a/lms/templates/university_profile/utx.html b/lms/templates/university_profile/utx.html new file mode 100644 index 0000000000..b9378f6ce3 --- /dev/null +++ b/lms/templates/university_profile/utx.html @@ -0,0 +1,24 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">UTx + +<%block name="university_header"> + + + + +<%block name="university_description"> +

    Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.

    + + +${parent.body()} diff --git a/lms/templates/video.html b/lms/templates/video.html index bd3ec77fbe..47556095cb 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -13,3 +13,8 @@ +% if source: +
    +

    Download video here.

    +
    +% endif diff --git a/lms/urls.py b/lms/urls.py index a5c06ad979..8cc81a438b 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -19,9 +19,9 @@ urlpatterns = ('', # (specifically missing get parameters in certain cases) url(r'^debug_request$', 'util.views.debug_request'), - url(r'^change_email$', 'student.views.change_email_request'), + url(r'^change_email$', 'student.views.change_email_request', name="change_email"), url(r'^email_confirm/(?P[^/]*)$', 'student.views.confirm_email_change'), - url(r'^change_name$', 'student.views.change_name_request'), + url(r'^change_name$', 'student.views.change_name_request', name="change_name"), url(r'^accept_name_change$', 'student.views.accept_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'), @@ -52,6 +52,7 @@ urlpatterns = ('', url(r'^heartbeat$', include('heartbeat.urls')), + url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'UTx'}), url(r'^university_profile/(?P[^/]+)$', 'courseware.views.university_profile', name="university_profile"), #Semi-static views (these need to be rendered and have the login bar, but don't change) @@ -88,7 +89,10 @@ urlpatterns = ('', {'template': 'press_releases/edX_announces_proctored_exam_testing.html'}, name="press/edX-announces-proctored-exam-testing"), url(r'^press/elsevier-collaborates-with-edx$', 'static_template_view.views.render', {'template': 'press_releases/Elsevier_collaborates_with_edX.html'}, name="press/elsevier-collaborates-with-edx"), - + url(r'^press/ut-joins-edx$', 'static_template_view.views.render', + {'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"), + url(r'^press/cengage-to-provide-book-content$', 'static_template_view.views.render', + {'template': 'press_releases/Cengage_to_provide_book_content.html'}, name="press/cengage-to-provide-book-content"), # Should this always update to point to the latest press release? (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), @@ -141,6 +145,24 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch', name='modx_dispatch'), + + # TODO (vshnayder): This is a hack. It creates a direct connection from + # the LMS to capa functionality, and really wants to go through the + # input types system so that previews can be context-specific. + # Unfortunately, we don't have time to think through the right way to do + # that (and implement it), and it's not a terrible thing to provide a + # generic chemican-equation rendering service. + url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc', + name='preview_chemcalc'), + + # Software Licenses + + # TODO: for now, this is the endpoint of an ajax replay + # service that retrieve and assigns license numbers for + # software assigned to a course. The numbers have to be loaded + # into the database. + url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback', name='xqueue_callback'), @@ -236,7 +258,7 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'): urlpatterns += ( url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'), - url(r'^openid/provider/login/(?:[\w%\. ]+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'), + url(r'^openid/provider/login/(?:.+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'), url(r'^openid/provider/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'), url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds') ) diff --git a/rakefile b/rakefile index f5e26a208f..90cd5fe87f 100644 --- a/rakefile +++ b/rakefile @@ -151,6 +151,13 @@ Dir["common/lib/*"].each do |lib| sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}") end TEST_TASKS << task_name + + desc "Run tests for common lib #{lib} (without coverage)" + task "fasttest_#{lib}" do + sh("nosetests #{lib}") + end + + end task :test do @@ -171,6 +178,12 @@ task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, a sh(django_admin(args.system, args.env, args.action, args.options)) end +desc "Set the staff bit for a user" +task :set_staff, [:user, :system, :env] do |t, args| + args.with_defaults(:env => 'dev', :system => 'lms', :options => '') + sh(django_admin(args.system, args.env, 'set_staff', args.user)) +end + task :package do FileUtils.mkdir_p(BUILD_DIR) diff --git a/requirements.txt b/requirements.txt index c3322c5b7c..2ebca50bc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ beautifulsoup beautifulsoup4 feedparser requests -sympy +http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz newrelic glob2 pymongo @@ -49,3 +49,4 @@ networkx pygraphviz -r repo-requirements.txt pil +nltk