diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 9e41d31c77..5cf21ca68d 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -4,19 +4,18 @@ import logging import random import re import string +import fnmatch from external_auth.models import ExternalAuthMap from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login -from django.contrib.auth.models import Group from django.contrib.auth.models import User +from student.models import UserProfile -from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import render_to_response +from django.utils.http import urlquote from django.shortcuts import redirect -from django.template import RequestContext from mitxmako.shortcuts import render_to_response, render_to_string try: from django.views.decorators.csrf import csrf_exempt @@ -24,100 +23,132 @@ except ImportError: from django.contrib.csrf.middleware import csrf_exempt from django_future.csrf import ensure_csrf_cookie from util.cache import cache_if_anonymous - -from django_openid_auth import auth as openid_auth -from openid.consumer.consumer import (Consumer, SUCCESS, CANCEL, FAILURE) + import django_openid_auth.views as openid_views +from django_openid_auth import auth as openid_auth +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 log = logging.getLogger("mitx.external_auth") + +# ----------------------------------------------------------------------------- +# OpenID Common +# ----------------------------------------------------------------------------- + + @csrf_exempt -def default_render_failure(request, message, status=403, template_name='extauth_failure.html', exception=None): - """Render an Openid error page to the user.""" - message = "In openid_failure " + message - log.debug(message) - data = render_to_string( template_name, dict(message=message, exception=exception)) +def default_render_failure(request, + message, + status=403, + template_name='extauth_failure.html', + exception=None): + """Render an Openid error page to the user""" + + log.debug("In openid_failure " + message) + + data = render_to_string(template_name, + dict(message=message, exception=exception)) + return HttpResponse(data, status=status) -#----------------------------------------------------------------------------- -# Openid -def edXauth_generate_password(length=12, chars=string.letters + string.digits): +# ----------------------------------------------------------------------------- +# OpenID Authentication +# ----------------------------------------------------------------------------- + + +def generate_password(length=12, chars=string.letters + string.digits): """Generate internal password for externally authenticated user""" - return ''.join([random.choice(chars) for i in range(length)]) + choice = random.SystemRandom().choice + return ''.join([choice(chars) for i in range(length)]) + @csrf_exempt -def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME, render_failure=None): +def openid_login_complete(request, + redirect_field_name=REDIRECT_FIELD_NAME, + render_failure=None): """Complete the openid login process""" - redirect_to = request.REQUEST.get(redirect_field_name, '') - render_failure = render_failure or \ - getattr(settings, 'OPENID_RENDER_FAILURE', None) or \ - default_render_failure - + render_failure = (render_failure or default_render_failure) + openid_response = openid_views.parse_openid_response(request) if not openid_response: - return render_failure(request, 'This is an OpenID relying party endpoint.') + return render_failure(request, + 'This is an OpenID relying party endpoint.') if openid_response.status == SUCCESS: external_id = openid_response.identity_url - oid_backend = openid_auth.OpenIDBackend() + oid_backend = openid_auth.OpenIDBackend() details = oid_backend._extract_user_details(openid_response) log.debug('openid success, details=%s' % details) - return edXauth_external_login_or_signup(request, - external_id, - "openid:%s" % settings.OPENID_SSO_SERVER_URL, - details, - details.get('email',''), - '%s %s' % (details.get('first_name',''),details.get('last_name','')) - ) - + url = getattr(settings, 'OPENID_SSO_SERVER_URL', None) + external_domain = "openid:%s" % url + fullname = '%s %s' % (details.get('first_name', ''), + details.get('last_name', '')) + + return external_login_or_signup(request, + external_id, + external_domain, + details, + details.get('email', ''), + fullname) + return render_failure(request, 'Openid failure') -#----------------------------------------------------------------------------- -# generic external auth login or signup -def edXauth_external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, - retfun=None): +def external_login_or_signup(request, + external_id, + external_domain, + credentials, + email, + fullname, + retfun=None): + """Generic external auth login or signup""" + # see if we have a map from this external_id to an edX username try: - eamap = ExternalAuthMap.objects.get(external_id = external_id, - external_domain = external_domain, - ) + eamap = ExternalAuthMap.objects.get(external_id=external_id, + external_domain=external_domain) log.debug('Found eamap=%s' % eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user - eamap = ExternalAuthMap(external_id = external_id, - external_domain = external_domain, - external_credentials = json.dumps(credentials), - ) + eamap = ExternalAuthMap(external_id=external_id, + external_domain=external_domain, + external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname - eamap.internal_password = edXauth_generate_password() - log.debug('created eamap=%s' % eamap) + eamap.internal_password = generate_password() + log.debug('Created eamap=%s' % eamap) eamap.save() internal_user = eamap.user if internal_user is None: - log.debug('ExtAuth: no user for %s yet, doing signup' % eamap.external_email) - return edXauth_signup(request, eamap) - + log.debug('No user for %s yet, doing signup' % eamap.external_email) + return signup(request, eamap) + uname = internal_user.username user = authenticate(username=uname, password=eamap.internal_password) if user is None: - log.warning("External Auth Login failed for %s / %s" % (uname,eamap.internal_password)) - return edXauth_signup(request, eamap) + log.warning("External Auth Login failed for %s / %s" % + (uname, eamap.internal_password)) + return signup(request, eamap) if not user.is_active: - log.warning("External Auth: user %s is not active" % (uname)) + log.warning("User %s is not active" % (uname)) # TODO: improve error page - return render_failure(request, 'Account not yet activated: please look for link in your email') - + msg = 'Account not yet activated: please look for link in your email' + return default_render_failure(request, msg) + login(request, user) request.session.set_expiry(0) student_views.try_change_enrollment(request) @@ -125,14 +156,11 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred if retfun is None: return redirect('/') return retfun() - - -#----------------------------------------------------------------------------- -# generic external auth signup + @ensure_csrf_cookie @cache_if_anonymous -def edXauth_signup(request, eamap=None): +def signup(request, eamap=None): """ Present form to complete for signup via external authentication. Even though the user has external credentials, he/she still needs @@ -142,32 +170,39 @@ def edXauth_signup(request, eamap=None): eamap is an ExteralAuthMap object, specifying the external user for which to complete the signup. """ - + if eamap is None: pass - request.session['ExternalAuthMap'] = eamap # save this for use by student.views.create_account - + # save this for use by student.views.create_account + request.session['ExternalAuthMap'] = eamap + + # default conjoin name, no spaces + username = eamap.external_name.replace(' ', '') + context = {'has_extauth_info': True, - 'show_signup_immediately' : True, + 'show_signup_immediately': True, 'extauth_email': eamap.external_email, - 'extauth_username' : eamap.external_name.replace(' ',''), # default - conjoin name, no spaces + 'extauth_username': username, 'extauth_name': eamap.external_name, } - - log.debug('ExtAuth: doing signup for %s' % eamap.external_email) + + log.debug('Doing signup for %s' % eamap.external_email) return student_views.index(request, extra_context=context) -#----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- # MIT SSL +# ----------------------------------------------------------------------------- + def ssl_dn_extract_info(dn): - ''' - Extract username, email address (may be anyuser@anydomain.com) and full name - from the SSL DN string. Return (user,email,fullname) if successful, and None - otherwise. - ''' + """ + Extract username, email address (may be anyuser@anydomain.com) and + full name from the SSL DN string. Return (user,email,fullname) if + successful, and None otherwise. + """ ss = re.search('/emailAddress=(.*)@([^/]+)', dn) if ss: user = ss.group(1) @@ -181,40 +216,333 @@ def ssl_dn_extract_info(dn): return None return (user, email, fullname) + @csrf_exempt -def edXauth_ssl_login(request): +def ssl_login(request): """ - This is called by student.views.index when MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True + This is called by student.views.index when + MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True - Used for MIT user authentication. This presumes the web server (nginx) has been configured - to require specific client certificates. + Used for MIT user authentication. This presumes the web server + (nginx) has been configured to require specific client + certificates. - If the incoming protocol is HTTPS (SSL) then authenticate via client certificate. - The certificate provides user email and fullname; this populates the ExternalAuthMap. - The user is nevertheless still asked to complete the edX signup. + If the incoming protocol is HTTPS (SSL) then authenticate via + client certificate. The certificate provides user email and + fullname; this populates the ExternalAuthMap. The user is + nevertheless still asked to complete the edX signup. Else continues on with student.views.index, and no authentication. """ - certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use - - cert = request.META.get(certkey,'') + certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use + + cert = request.META.get(certkey, '') if not cert: - cert = request.META.get('HTTP_'+certkey,'') + cert = request.META.get('HTTP_' + certkey, '') if not cert: try: - cert = request._req.subprocess_env.get(certkey,'') # try the direct apache2 SSL key - except Exception as err: - pass + # try the direct apache2 SSL key + cert = request._req.subprocess_env.get(certkey, '') + except Exception: + cert = None + if not cert: # no certificate information - go onward to main index return student_views.index(request) (user, email, fullname) = ssl_dn_extract_info(cert) - - return edXauth_external_login_or_signup(request, - external_id=email, - external_domain="ssl:MIT", - credentials=cert, - email=email, - fullname=fullname, - retfun = functools.partial(student_views.index, request)) + + retfun = functools.partial(student_views.index, request) + return external_login_or_signup(request, + external_id=email, + external_domain="ssl:MIT", + credentials=cert, + email=email, + fullname=fullname, + retfun=retfun) + + +# ----------------------------------------------------------------------------- +# OpenID Provider +# ----------------------------------------------------------------------------- + + +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 + + location = host + '/openid/provider/' + resource + '/' + + if request.is_secure(): + return 'https://' + location + else: + return 'http://' + location + + +def add_openid_simple_registration(request, response, data): + sreg_data = {} + sreg_request = sreg.SRegRequest.fromOpenIDRequest(request) + sreg_fields = sreg_request.allRequestedFields() + + # if consumer requested simple registration fields, add them + if sreg_fields: + for field in sreg_fields: + if field == 'email' and 'email' in data: + sreg_data['email'] = data['email'] + elif field == 'fullname' and 'fullname' in data: + sreg_data['fullname'] = data['fullname'] + + # construct sreg response + sreg_response = sreg.SRegResponse.extractResponse(sreg_request, + sreg_data) + sreg_response.toMessage(response.fields) + + +def add_openid_attribute_exchange(request, response, data): + try: + ax_request = ax.FetchRequest.fromOpenIDRequest(request) + except ax.AXError: + # not using OpenID attribute exchange extension + pass + else: + ax_response = ax.FetchResponse() + + # if consumer requested attribute exchange fields, add them + if ax_request and ax_request.requested_attributes: + for type_uri in ax_request.requested_attributes.iterkeys(): + email_schema = 'http://axschema.org/contact/email' + name_schema = 'http://axschema.org/namePerson' + if type_uri == email_schema and 'email' in data: + ax_response.addValue(email_schema, data['email']) + elif type_uri == name_schema and 'fullname' in data: + ax_response.addValue(name_schema, data['fullname']) + + # construct ax response + ax_response.toMessage(response.fields) + + +def provider_respond(server, request, response, data): + """ + Respond to an OpenID request + """ + # get and add extensions + add_openid_simple_registration(request, response, data) + add_openid_attribute_exchange(request, response, data) + + # create http response from OpenID response + webresponse = server.encodeResponse(response) + http_response = HttpResponse(webresponse.body) + http_response.status_code = webresponse.code + + # add OpenID headers to response + for k, v in webresponse.headers.iteritems(): + http_response[k] = v + + return http_response + + +def validate_trust_root(openid_request): + """ + Only allow OpenID requests from valid trust roots + """ + + trusted_roots = getattr(settings, 'OPENID_PROVIDER_TRUSTED_ROOT', None) + + if not trusted_roots: + # not using trusted roots + return True + + # don't allow empty trust roots + if (not hasattr(openid_request, 'trust_root') or + not openid_request.trust_root): + log.error('no trust_root') + return False + + # ensure trust root parses cleanly (one wildcard, of form *.foo.com, etc.) + trust_root = TrustRoot.parse(openid_request.trust_root) + if not trust_root: + log.error('invalid trust_root') + return False + + # don't allow empty return tos + if (not hasattr(openid_request, 'return_to') or + not openid_request.return_to): + log.error('empty return_to') + return False + + # ensure return to is within trust root + if not trust_root.validateURL(openid_request.return_to): + log.error('invalid return_to') + return False + + # check that the root matches the ones we trust + if not any(r for r in trusted_roots if fnmatch.fnmatch(trust_root, r)): + log.error('non-trusted root') + return False + + return True + + +@csrf_exempt +def provider_login(request): + """ + OpenID login endpoint + """ + + # make and validate endpoint + endpoint = get_xrds_url('login', request) + if not endpoint: + return default_render_failure(request, "Invalid OpenID request") + + # initialize store and server + store = FileOpenIDStore('/tmp/openid_provider') + server = Server(store, endpoint) + + # handle OpenID request + querydict = dict(request.REQUEST.items()) + error = False + if 'openid.mode' in request.GET or 'openid.mode' in request.POST: + # decode request + openid_request = server.decodeRequest(querydict) + + if not openid_request: + return default_render_failure(request, "Invalid OpenID request") + + # don't allow invalid and non-trusted trust roots + if not validate_trust_root(openid_request): + return default_render_failure(request, "Invalid OpenID trust root") + + # checkid_immediate not supported, require user interaction + if openid_request.mode == 'checkid_immediate': + return provider_respond(server, openid_request, + openid_request.answer(False), {}) + + # checkid_setup, so display login page + elif openid_request.mode == 'checkid_setup': + if openid_request.idSelect(): + # remember request and original path + request.session['openid_setup'] = { + 'request': openid_request, + 'url': request.get_full_path() + } + + # user failed login on previous attempt + if 'openid_error' in request.session: + error = True + del request.session['openid_error'] + + # OpenID response + else: + return provider_respond(server, openid_request, + server.handleRequest(openid_request), {}) + + # handle login + if request.method == 'POST' and 'openid_setup' in request.session: + # get OpenID request from session + openid_setup = request.session['openid_setup'] + openid_request = openid_setup['request'] + openid_request_url = openid_setup['url'] + del request.session['openid_setup'] + + # don't allow invalid trust roots + if not validate_trust_root(openid_request): + return default_render_failure(request, "Invalid OpenID trust root") + + # check if user with given email exists + email = request.POST.get('email', None) + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + request.session['openid_error'] = True + msg = "OpenID login failed - Unknown user email: {0}".format(email) + log.warning(msg) + return HttpResponseRedirect(openid_request_url) + + # attempt to authenticate user + username = user.username + password = request.POST.get('password', None) + user = authenticate(username=username, password=password) + if user is None: + request.session['openid_error'] = True + msg = "OpenID login failed - password for {0} is invalid" + msg = msg.format(email) + log.warning(msg) + return HttpResponseRedirect(openid_request_url) + + # authentication succeeded, so log user in + if user is not None and user.is_active: + # remove error from session since login succeeded + if 'openid_error' in request.session: + del request.session['openid_error'] + + # fullname field comes from user profile + profile = UserProfile.objects.get(user=user) + log.info("OpenID login success - {0} ({1})".format(user.username, + user.email)) + + # redirect user to return_to location + 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 + }) + + request.session['openid_error'] = True + msg = "Login failed - Account not active for user {0}".format(username) + log.warning(msg) + return HttpResponseRedirect(openid_request_url) + + # determine consumer domain if applicable + return_to = '' + if 'openid.return_to' in request.REQUEST: + return_to = request.REQUEST['openid.return_to'] + matches = re.match(r'\w+:\/\/([\w\.-]+)', return_to) + return_to = matches.group(1) + + # display login page + response = render_to_response('provider_login.html', { + 'error': error, + 'return_to': return_to + }) + + # custom XRDS header necessary for discovery process + response['X-XRDS-Location'] = get_xrds_url('xrds', request) + return response + + +def provider_identity(request): + """ + XRDS for identity discovery + """ + + response = render_to_response('identity.xml', + {'url': get_xrds_url('login', request)}, + mimetype='text/xml') + + # custom XRDS header necessary for discovery process + response['X-XRDS-Location'] = get_xrds_url('identity', request) + return response + + +def provider_xrds(request): + """ + XRDS for endpoint discovery + """ + + response = render_to_response('xrds.xml', + {'url': get_xrds_url('login', request)}, + mimetype='text/xml') + + # custom XRDS header necessary for discovery process + response['X-XRDS-Location'] = get_xrds_url('xrds', request) + return response diff --git a/common/lib/capa/capa/templates/filesubmission.html b/common/lib/capa/capa/templates/filesubmission.html index e9fd7c5674..630a3222dc 100644 --- a/common/lib/capa/capa/templates/filesubmission.html +++ b/common/lib/capa/capa/templates/filesubmission.html @@ -1,17 +1,18 @@
-
+
% if state == 'unsubmitted': - + Unanswered % elif state == 'correct': - + Correct % elif state == 'incorrect': - + Incorrect % elif state == 'queued': - + Queued % endif - -
- ${msg|n} -
+

${state}

+ + +
+
${msg|n}
diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/textbox.html index 19c43482a8..271d7795e0 100644 --- a/common/lib/capa/capa/templates/textbox.html +++ b/common/lib/capa/capa/templates/textbox.html @@ -7,26 +7,28 @@ - % if state == 'unsubmitted': - - % elif state == 'correct': - - % elif state == 'incorrect': - - % elif state == 'queued': - - - % endif - % if hidden: -
- % endif -
- -
- ${msg|n} -
+
+ % if state == 'unsubmitted': + Unanswered + % elif state == 'correct': + Correct + % elif state == 'incorrect': + Incorrect + % elif state == 'queued': + Queued + + % endif -
+ % if hidden: +
+ % endif + +

${state}

+
+ +
+ ${msg|n} +
- diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index e6ebdb316f..aea59a4d63 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -16,6 +16,7 @@ h2 { } } + section.problem { @media print { display: block; @@ -31,6 +32,7 @@ section.problem { display: inline; } + div { p { &.answer { @@ -171,8 +173,54 @@ section.problem { top: 6px; } } + + .grader-status { + padding: 9px; + background: #F6F6F6; + border: 1px solid #ddd; + border-top: 0; + margin-bottom: 20px; + @include clearfix; + + span { + text-indent: -9999px; + overflow: hidden; + display: block; + float: left; + margin: -7px 7px 0 0; + } + + p { + line-height: 20px; + text-transform: capitalize; + margin-bottom: 0; + float: left; + } + + &.file { + background: #FFF; + margin-top: 20px; + padding: 20px 0 0 0; + + border: { + top: 1px solid #eee; + right: 0; + bottom: 0; + left: 0; + } + + p.debug { + display: none; + } + + input { + float: left; + } + } + } } + ul { list-style: disc outside none; margin-bottom: lh(); @@ -246,6 +294,69 @@ section.problem { } + code { + margin: 0 2px; + padding: 0px 5px; + white-space: nowrap; + border: 1px solid #EAEAEA; + background-color: #F8F8F8; + @include border-radius(3px); + font-size: .9em; + } + + pre { + background-color: #F8F8F8; + border: 1px solid #CCC; + font-size: .9em; + line-height: 1.4; + overflow: auto; + padding: 6px 10px; + @include border-radius(3px); + + > code { + margin: 0; + padding: 0; + white-space: pre; + border: none; + background: transparent; + } + } + + .CodeMirror { + border: 1px solid black; + font-size: 14px; + line-height: 18px; + resize: both; + + pre { + @include border-radius(0); + border-radius: 0; + border-width: 0; + margin: 0; + padding: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + white-space: pre; + word-wrap: normal; + overflow: hidden; + resize: none; + + &.CodeMirror-cursor { + z-index: 10; + position: absolute; + visibility: hidden; + border-left: 1px solid black; + border-right: none; + width: 0; + } + } + } + + .CodeMirror-focused pre.CodeMirror-cursor { + visibility: visible; + } + hr { background: #ddd; border: none; @@ -280,4 +391,96 @@ section.problem { @extend .blue-button; } } + + div.capa_alert { + padding: 8px 12px; + border: 1px solid #EBE8BF; + border-radius: 3px; + background: #FFFCDD; + font-size: 0.9em; + margin-top: 10px; + } + + .hints { + border: 1px solid #ccc; + + h3 { + border-bottom: 1px solid #e3e3e3; + text-shadow: 0 1px 0 #fff; + padding: 9px; + background: #eee; + font-weight: bold; + font-size: em(16); + } + + div { + border-bottom: 1px solid #ddd; + + &:last-child { + border-bottom: none; + } + + p { + margin-bottom: 0; + } + + header { + a { + display: block; + padding: 9px; + background: #F6F6F6; + @include box-shadow(inset 0 0 0 1px #fff); + } + } + + section { + padding: 9px; + } + } + } + + .test { + padding-top: 18px; + + header { + margin-bottom: 12px; + + h3 { + font-size: 0.9em; + font-weight: bold; + font-style: normal; + text-transform: uppercase; + color: #AAA; + } + } + + > section { + border: 1px solid #ddd; + padding: 9px 9px 20px; + margin-bottom: 10px; + background: #FFF; + position: relative; + @include box-shadow(inset 0 0 0 1px #eee); + @include border-radius(3px); + + p:last-of-type { + margin-bottom: 0; + } + + .shortform { + margin-bottom: .6em; + } + + a.full { + @include position(absolute, 0 0 1px 0px); + font-size: .8em; + padding: 4px; + text-align: right; + width: 100%; + display: block; + background: #F3F3F3; + @include box-sizing(border-box); + } + } + } } diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 7b8dc5f57c..25d2c26dda 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -37,7 +37,6 @@ nav.sequence-nav { height: 44px; margin: 0 30px; @include linear-gradient(top, #ddd, #eee); - overflow: hidden; @include box-shadow(0 1px 3px rgba(0, 0, 0, .1) inset); } diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 0ea6cffb58..098b79d9cf 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -263,8 +263,8 @@ class @Problem @el.find('.capa_alert').remove() alert_elem = "
" + msg + "
" @el.find('.action').after(alert_elem) - @el.find('.capa_alert').animate(opacity: 0, 500).animate(opacity: 1, 500) - + @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) + save: => Logger.log 'problem_save', @answers $.postWithPrefix "#{@url}/problem_save", @answers, (response) => diff --git a/common/lib/xmodule/xmodule/modulestore/search.py b/common/lib/xmodule/xmodule/modulestore/search.py index baf3d46b57..f9901e8bfe 100644 --- a/common/lib/xmodule/xmodule/modulestore/search.py +++ b/common/lib/xmodule/xmodule/modulestore/search.py @@ -6,15 +6,14 @@ from .exceptions import (ItemNotFoundError, NoPathToItem) from . import ModuleStore, Location -def path_to_location(modulestore, location, course_name=None): +def path_to_location(modulestore, course_id, location): ''' Try to find a course_id/chapter/section[/position] path to location in modulestore. The courseware insists that the first level in the course is chapter, but any kind of module can be a "section". location: something that can be passed to Location - course_name: [optional]. If not None, restrict search to paths - in that course. + course_id: Search for paths in this course. raise ItemNotFoundError if the location doesn't exist. @@ -27,7 +26,7 @@ def path_to_location(modulestore, location, course_name=None): A location may be accessible via many paths. This method may return any valid path. - If the section is a sequence, position will be the position + If the section is a sequential or vertical, position will be the position of this location in that sequence. Otherwise, position will be None. TODO (vshnayder): Not true yet. ''' @@ -41,7 +40,7 @@ def path_to_location(modulestore, location, course_name=None): xs = xs[1] return p - def find_path_to_course(location, course_name=None): + def find_path_to_course(): '''Find a path up the location graph to a node with the specified category. @@ -69,7 +68,8 @@ def path_to_location(modulestore, location, course_name=None): # print 'Processing loc={0}, path={1}'.format(loc, path) if loc.category == "course": - if course_name is None or course_name == loc.name: + # confirm that this is the right course + if course_id == CourseDescriptor.location_to_id(loc): # Found it! path = (loc, path) return flatten(path) @@ -81,17 +81,34 @@ def path_to_location(modulestore, location, course_name=None): # If we're here, there is no path return None - path = find_path_to_course(location, course_name) + path = find_path_to_course() if path is None: - raise(NoPathToItem(location)) + raise NoPathToItem(location) n = len(path) course_id = CourseDescriptor.location_to_id(path[0]) # pull out the location names chapter = path[1].name if n > 1 else None section = path[2].name if n > 2 else None - - # TODO (vshnayder): not handling position at all yet... + # Figure out the position position = None + # This block of code will find the position of a module within a nested tree + # of modules. If a problem is on tab 2 of a sequence that's on tab 3 of a + # sequence, the resulting position is 3_2. However, no positional modules + # (e.g. sequential and videosequence) currently deal with this form of + # representing nested positions. This needs to happen before jumping to a + # module nested in more than one positional module will work. + if n > 3: + position_list = [] + for path_index in range(2, n-1): + category = path[path_index].category + if category == 'sequential' or category == 'videosequence': + section_desc = modulestore.get_instance(course_id, path[path_index]) + child_locs = [c.location for c in section_desc.get_children()] + # positions are 1-indexed, and should be strings to be consistent with + # url parsing. + position_list.append(str(child_locs.index(path[path_index+1]) + 1)) + position = "_".join(position_list) + return (course_id, chapter, section, position) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/__init__.py b/common/lib/xmodule/xmodule/modulestore/tests/__init__.py new file mode 100644 index 0000000000..126f0136e2 --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/tests/__init__.py @@ -0,0 +1,12 @@ +from path import path + +# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/ +# to ~/mitx_all/mitx/common/test +TEST_DIR = path(__file__).abspath().dirname() +for i in range(5): + TEST_DIR = TEST_DIR.dirname() +TEST_DIR = TEST_DIR / 'test' + +DATA_DIR = TEST_DIR / 'data' + + diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py new file mode 100644 index 0000000000..c1d1d50a53 --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py @@ -0,0 +1,34 @@ +from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup + +from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem +from xmodule.modulestore.search import path_to_location + +def check_path_to_location(modulestore): + '''Make sure that path_to_location works: should be passed a modulestore + with the toy and simple courses loaded.''' + should_work = ( + ("i4x://edX/toy/video/Welcome", + ("edX/toy/2012_Fall", "Overview", "Welcome", None)), + ("i4x://edX/toy/chapter/Overview", + ("edX/toy/2012_Fall", "Overview", None, None)), + ) + course_id = "edX/toy/2012_Fall" + + for location, expected in should_work: + assert_equals(path_to_location(modulestore, course_id, location), expected) + + not_found = ( + "i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome" + ) + for location in not_found: + assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location) + + # Since our test files are valid, there shouldn't be any + # elements with no path to them. But we can look for them in + # another course. + no_path = ( + "i4x://edX/simple/video/Lost_Video", + ) + for location in no_path: + assert_raises(NoPathToItem, path_to_location, modulestore, course_id, location) + diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 746240e763..4c593e391e 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -1,23 +1,14 @@ import pymongo from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup -from path import path from pprint import pprint from xmodule.modulestore import Location -from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.search import path_to_location -# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/ -# to ~/mitx_all/mitx/common/test -TEST_DIR = path(__file__).abspath().dirname() -for i in range(5): - TEST_DIR = TEST_DIR.dirname() -TEST_DIR = TEST_DIR / 'test' - -DATA_DIR = TEST_DIR / 'data' +from .test_modulestore import check_path_to_location +from . import DATA_DIR HOST = 'localhost' @@ -110,27 +101,5 @@ class TestMongoModuleStore(object): def test_path_to_location(self): '''Make sure that path_to_location works''' - should_work = ( - ("i4x://edX/toy/video/Welcome", - ("edX/toy/2012_Fall", "Overview", "Welcome", None)), - ("i4x://edX/toy/chapter/Overview", - ("edX/toy/2012_Fall", "Overview", None, None)), - ) - for location, expected in should_work: - assert_equals(path_to_location(self.store, location), expected) - - not_found = ( - "i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome" - ) - for location in not_found: - assert_raises(ItemNotFoundError, path_to_location, self.store, location) - - # Since our test files are valid, there shouldn't be any - # elements with no path to them. But we can look for them in - # another course. - no_path = ( - "i4x://edX/simple/video/Lost_Video", - ) - for location in no_path: - assert_raises(NoPathToItem, path_to_location, self.store, location, "toy") + check_path_to_location(self.store) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py new file mode 100644 index 0000000000..c4446bebb5 --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py @@ -0,0 +1,16 @@ +from xmodule.modulestore import Location +from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.xml_importer import import_from_xml + +from .test_modulestore import check_path_to_location +from . import DATA_DIR + +class TestXMLModuleStore(object): + def test_path_to_location(self): + """Make sure that path_to_location works properly""" + + print "Starting import" + modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple']) + print "finished import" + + check_path_to_location(modulestore) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 92eca8f5e6..23a5473292 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -37,7 +37,7 @@ def clean_out_mako_templating(xml_string): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): def __init__(self, xmlstore, course_id, course_dir, - policy, error_tracker, **kwargs): + policy, error_tracker, parent_tracker, **kwargs): """ A class that handles loading from xml. Does some munging to ensure that all elements have unique slugs. @@ -79,11 +79,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): del attr[key] break - def fallback_name(): + def fallback_name(orig_name=None): """Return the fallback name for this module. This is a function instead of a variable because we want it to be lazy.""" - # use the hash of the content--the first 12 bytes should be plenty. - return tag + "_" + hashlib.sha1(xml).hexdigest()[:12] + # append the hash of the content--the first 12 bytes should be plenty. + orig_name = "_" + orig_name if orig_name is not None else "" + return tag + orig_name + "_" + hashlib.sha1(xml).hexdigest()[:12] # Fallback if there was nothing we could use: if url_name is None or url_name == "": @@ -93,8 +94,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): need_uniq_names = ('problem', 'sequence', 'video', 'course', 'chapter') if tag in need_uniq_names: - error_tracker("ERROR: no name of any kind specified for {tag}. Student " - "state won't work right. Problem xml: '{xml}...'".format(tag=tag, xml=xml[:100])) + error_tracker("PROBLEM: no name of any kind specified for {tag}. Student " + "state will not be properly tracked for this module. Problem xml:" + " '{xml}...'".format(tag=tag, xml=xml[:100])) else: # TODO (vshnayder): We may want to enable this once course repos are cleaned up. # (or we may want to give up on the requirement for non-state-relevant issues...) @@ -103,13 +105,20 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): # Make sure everything is unique if url_name in self.used_names[tag]: - msg = ("Non-unique url_name in xml. This may break content. url_name={0}. Content={1}" - .format(url_name, xml[:100])) - error_tracker("ERROR: " + msg) + msg = ("Non-unique url_name in xml. This may break state tracking for content." + " url_name={0}. Content={1}".format(url_name, xml[:100])) + error_tracker("PROBLEM: " + msg) log.warning(msg) # Just set name to fallback_name--if there are multiple things with the same fallback name, # they are actually identical, so it's fragile, but not immediately broken. - url_name = fallback_name() + + # TODO (vshnayder): if the tag is a pointer tag, this will + # break the content because we won't have the right link. + # That's also a legitimate attempt to reuse the same content + # from multiple places. Once we actually allow that, we'll + # need to update this to complain about non-unique names for + # definitions, but allow multiple uses. + url_name = fallback_name(url_name) self.used_names[tag].add(url_name) xml_data.set('url_name', url_name) @@ -134,8 +143,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): xmlstore.modules[course_id][descriptor.location] = descriptor - if xmlstore.eager: - descriptor.get_children() + for child in descriptor.get_children(): + parent_tracker.add_parent(child.location, descriptor.location) return descriptor render_template = lambda: '' @@ -151,12 +160,51 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): error_tracker, process_xml, policy, **kwargs) +class ParentTracker(object): + """A simple class to factor out the logic for tracking location parent pointers.""" + def __init__(self): + """ + Init + """ + # location -> set(parents). Not using defaultdict because we care about the empty case. + self._parents = dict() + + def add_parent(self, child, parent): + """ + Add a parent of child location to the set of parents. Duplicate calls have no effect. + + child and parent must be something that can be passed to Location. + """ + child = Location(child) + parent = Location(parent) + s = self._parents.setdefault(child, set()) + s.add(parent) + + def is_known(self, child): + """ + returns True iff child has some parents. + """ + child = Location(child) + return child in self._parents + + def make_known(self, location): + """Tell the parent tracker about an object, without registering any + parents for it. Used for the top level course descriptor locations.""" + self._parents.setdefault(location, set()) + + def parents(self, child): + """ + Return a list of the parents of this child. If not is_known(child), will throw a KeyError + """ + child = Location(child) + return list(self._parents[child]) + + class XMLModuleStore(ModuleStoreBase): """ An XML backed ModuleStore """ - def __init__(self, data_dir, default_class=None, eager=False, - course_dirs=None): + def __init__(self, data_dir, default_class=None, course_dirs=None): """ Initialize an XMLModuleStore from data_dir @@ -165,15 +213,11 @@ class XMLModuleStore(ModuleStoreBase): default_class: dot-separated string defining the default descriptor class to use if none is specified in entry_points - eager: If true, load the modules children immediately to force the - entire course tree to be parsed - course_dirs: If specified, the list of course_dirs to load. Otherwise, load all course dirs """ ModuleStoreBase.__init__(self) - self.eager = eager self.data_dir = path(data_dir) self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor) self.courses = {} # course_dir -> XModuleDescriptor for the course @@ -186,10 +230,7 @@ class XMLModuleStore(ModuleStoreBase): class_ = getattr(import_module(module_path), class_name) self.default_class = class_ - # TODO (cpennington): We need a better way of selecting specific sets of - # debug messages to enable. These were drowning out important messages - #log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir)) - #log.debug('default_class = %s' % self.default_class) + self.parent_tracker = ParentTracker() # If we are specifically asked for missing courses, that should # be an error. If we are asked for "all" courses, find the ones @@ -221,6 +262,7 @@ class XMLModuleStore(ModuleStoreBase): if course_descriptor is not None: self.courses[course_dir] = course_descriptor self._location_errors[course_descriptor.location] = errorlog + self.parent_tracker.make_known(course_descriptor.location) else: # Didn't load course. Instead, save the errors elsewhere. self.errored_courses[course_dir] = errorlog @@ -339,7 +381,7 @@ class XMLModuleStore(ModuleStoreBase): course_id = CourseDescriptor.make_id(org, course, url_name) - system = ImportSystem(self, course_id, course_dir, policy, tracker) + system = ImportSystem(self, course_id, course_dir, policy, tracker, self.parent_tracker) course_descriptor = system.process_xml(etree.tostring(course_data)) @@ -450,3 +492,19 @@ class XMLModuleStore(ModuleStoreBase): metadata: A nested dictionary of module metadata """ raise NotImplementedError("XMLModuleStores are read-only") + + def get_parent_locations(self, location): + '''Find all locations that are the parents of this location. Needed + for path_to_location(). + + If there is no data at location in this modulestore, raise + ItemNotFoundError. + + returns an iterable of things that can be passed to Location. This may + be empty if there are no parents. + ''' + location = Location.ensure_fully_specified(location) + if not self.parent_tracker.is_known(location): + raise ItemNotFoundError(location) + + return self.parent_tracker.parents(location) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 89f94d8cdb..be0bdc24c2 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -6,7 +6,7 @@ from .exceptions import DuplicateItemError log = logging.getLogger(__name__) -def import_from_xml(store, data_dir, course_dirs=None, eager=True, +def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor'): """ Import the specified xml data_dir into the "store" modulestore, @@ -19,7 +19,6 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True, module_store = XMLModuleStore( data_dir, default_class=default_class, - eager=eager, course_dirs=course_dirs ) for course_id in module_store.modules.keys(): diff --git a/common/lib/xmodule/xmodule/stringify.py b/common/lib/xmodule/xmodule/stringify.py index dad964140f..1e3fa91210 100644 --- a/common/lib/xmodule/xmodule/stringify.py +++ b/common/lib/xmodule/xmodule/stringify.py @@ -12,9 +12,17 @@ def stringify_children(node): fixed from http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml ''' - parts = ([node.text] + - list(chain(*([etree.tostring(c), c.tail] - for c in node.getchildren()) - ))) + # Useful things to know: + + # node.tostring() -- generates xml for the node, including start + # and end tags. We'll use this for the children. + # node.text -- the text after the end of a start tag to the start + # of the first child + # node.tail -- the text after the end this tag to the start of the + # next element. + parts = [node.text] + for c in node.getchildren(): + parts.append(etree.tostring(c, with_tail=True)) + # filter removes possible Nones in texts and tails return ''.join(filter(None, parts)) diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index 2520d95937..826e6c9d5a 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -49,7 +49,7 @@ class RoundTripTestCase(unittest.TestCase): copytree(data_dir / course_dir, root_dir / course_dir) print "Starting import" - initial_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir]) + initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) courses = initial_import.get_courses() self.assertEquals(len(courses), 1) @@ -66,7 +66,7 @@ class RoundTripTestCase(unittest.TestCase): course_xml.write(xml) print "Starting second import" - second_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir]) + second_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) courses2 = second_import.get_courses() self.assertEquals(len(courses2), 1) diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index a369850209..e81d82bf9e 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -193,7 +193,7 @@ class ImportTestCase(unittest.TestCase): """Make sure that metadata is inherited properly""" print "Starting import" - initial_import = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy']) + initial_import = XMLModuleStore(DATA_DIR, course_dirs=['toy']) courses = initial_import.get_courses() self.assertEquals(len(courses), 1) @@ -216,7 +216,7 @@ class ImportTestCase(unittest.TestCase): def get_course(name): print "Importing {0}".format(name) - modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=[name]) + modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name]) courses = modulestore.get_courses() self.assertEquals(len(courses), 1) return courses[0] @@ -245,7 +245,7 @@ class ImportTestCase(unittest.TestCase): happen--locations should uniquely name definitions. But in our imperfect XML world, it can (and likely will) happen.""" - modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy', 'two_toys']) + modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'two_toys']) toy_id = "edX/toy/2012_Fall" two_toy_id = "edX/toy/TT_2012_Fall" @@ -261,7 +261,7 @@ class ImportTestCase(unittest.TestCase): """Ensure that colons in url_names convert to file paths properly""" print "Starting import" - modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy']) + modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy']) courses = modulestore.get_courses() self.assertEquals(len(courses), 1) diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py index 1c6ee855f3..29e99bef56 100644 --- a/common/lib/xmodule/xmodule/tests/test_stringify.py +++ b/common/lib/xmodule/xmodule/tests/test_stringify.py @@ -1,4 +1,4 @@ -from nose.tools import assert_equals +from nose.tools import assert_equals, assert_true, assert_false from lxml import etree from xmodule.stringify import stringify_children @@ -8,3 +8,32 @@ def test_stringify(): xml = etree.fromstring(html) out = stringify_children(xml) assert_equals(out, text) + +def test_stringify_again(): + html = """A voltage source is non-linear! +
+ + \(V=V_C\) +
+ But it is affine, + which means linear except for an offset. + +""" + + html = """A voltage source is non-linear! +
+ +
+ But it is affine, + which means linear except for an offset. + + """ + xml = etree.fromstring(html) + out = stringify_children(xml) + + print "output:" + print out + + # Tracking strange content repeating bug + # Should appear once + assert_equals(out.count("But it is "), 1) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 0dc16bd976..82f623e977 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -544,7 +544,13 @@ class XModuleDescriptor(Plugin, HTMLSnippet): # Put import here to avoid circular import errors from xmodule.error_module import ErrorDescriptor msg = "Error loading from xml." - log.warning(msg + " " + str(err)) + log.warning(msg + " " + str(err)[:200]) + + # Normally, we don't want lots of exception traces in our logs from common + # content problems. But if you're debugging the xml loading code itself, + # uncomment the next line. + # log.exception(msg) + system.error_tracker(msg) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, diff --git a/common/test/data/full/chapter/Overview.xml b/common/test/data/full/chapter/Overview.xml index 89917d20da..a11a11a1e0 100644 --- a/common/test/data/full/chapter/Overview.xml +++ b/common/test/data/full/chapter/Overview.xml @@ -1,5 +1,5 @@ -
- -
-
-
-
-
-
-
-
- ##

60% complete

-
-
- Unregister diff --git a/lms/templates/identity.xml b/lms/templates/identity.xml new file mode 100644 index 0000000000..a925493c03 --- /dev/null +++ b/lms/templates/identity.xml @@ -0,0 +1,10 @@ + + + + + http://specs.openid.net/auth/2.0/signon + http://openid.net/signon/1.1 + ${url} + + + diff --git a/lms/templates/portal/course_about.html b/lms/templates/portal/course_about.html index 4931f1fed6..bff24d597a 100644 --- a/lms/templates/portal/course_about.html +++ b/lms/templates/portal/course_about.html @@ -74,7 +74,7 @@ %if show_link: %endif - You are registered for this course (${course.number}). + You are registered for this course (${course.number}) View Courseware %if show_link: %endif diff --git a/lms/templates/problem.html b/lms/templates/problem.html index ed49b3bd5d..65b8193df9 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -32,3 +32,27 @@ +<%block name="js_extra"> + + diff --git a/lms/templates/provider_login.html b/lms/templates/provider_login.html new file mode 100644 index 0000000000..620e0c4191 --- /dev/null +++ b/lms/templates/provider_login.html @@ -0,0 +1,52 @@ +<%inherit file="main.html" /> +<%namespace name='static' file='static_content.html'/> + +<%block name="headextra"> + + + + +
diff --git a/lms/templates/xrds.xml b/lms/templates/xrds.xml new file mode 100644 index 0000000000..2f7713bc8a --- /dev/null +++ b/lms/templates/xrds.xml @@ -0,0 +1,11 @@ + + + + + http://specs.openid.net/auth/2.0/server + http://openid.net/sreg/1.0 + http://openid.net/srv/ax/1.0 + ${url} + + + diff --git a/lms/urls.py b/lms/urls.py index 86d654eb40..278239751b 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -98,8 +98,9 @@ if settings.COURSEWARE_ENABLED: urlpatterns += ( # Hook django-masquerade, allowing staff to view site as other users url(r'^masquerade/', include('masquerade.urls')), - url(r'^jump_to/(?P.*)$', 'courseware.views.jump_to', name="jump_to"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/jump_to/(?P.*)$', + 'courseware.views.jump_to', name="jump_to"), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch', name='modx_dispatch'), @@ -142,6 +143,8 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.index', name="courseware_chapter"), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/(?P[^/]*)/(?P
[^/]*)/$', 'courseware.views.index', name="courseware_section"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/(?P[^/]*)/(?P
[^/]*)/(?P[^/]*)/?$', + 'courseware.views.index', name="courseware_position"), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/progress$', 'courseware.views.progress', name="progress"), # Takes optional student_id for instructor use--shows profile as that student sees it. @@ -164,7 +167,7 @@ if settings.COURSEWARE_ENABLED: if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): urlpatterns += ( - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/news$', + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/news$', 'courseware.views.news', name="news"), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/discussion/', include('django_comment_client.urls')) @@ -215,9 +218,17 @@ if settings.DEBUG: if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): urlpatterns += ( url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), - url(r'^openid/complete/$', 'external_auth.views.edXauth_openid_login_complete', name='openid-complete'), + url(r'^openid/complete/$', 'external_auth.views.openid_login_complete', name='openid-complete'), url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'), - ) + ) + +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/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'), + url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds') + ) if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): urlpatterns += (