Merge master
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<section id="filesubmission_${id}" class="filesubmission">
|
||||
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/><br />
|
||||
<div class="grader-status file">
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif state == 'queued':
|
||||
<span class="processing" id="status_${id}"></span>
|
||||
<span class="processing" id="status_${id}">Queued</span>
|
||||
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
|
||||
% endif
|
||||
<span style="display:none;" class="debug">(${state})</span>
|
||||
<br/>
|
||||
<span class="message">${msg|n}</span>
|
||||
<br/>
|
||||
<p class="debug">${state}</p>
|
||||
|
||||
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/>
|
||||
</div>
|
||||
<div class="message">${msg|n}</div>
|
||||
</section>
|
||||
|
||||
@@ -7,26 +7,28 @@
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'queued':
|
||||
<span class="processing" id="status_${id}"></span>
|
||||
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
<br/>
|
||||
<span style="display:none;" class="debug">(${state})</span>
|
||||
<br/>
|
||||
<span class="message">${msg|n}</span>
|
||||
<br/>
|
||||
<div class="grader-status">
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif state == 'queued':
|
||||
<span class="processing" id="status_${id}">Queued</span>
|
||||
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
|
||||
% endif
|
||||
|
||||
<br/>
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
|
||||
<p class="debug">${state}</p>
|
||||
</div>
|
||||
|
||||
<div class="external-grader-message">
|
||||
${msg|n}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to work.
|
||||
@@ -45,12 +47,4 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.CodeMirror {
|
||||
border: 1px solid black;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
resize: both;
|
||||
}
|
||||
</style>
|
||||
</section>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,8 +263,8 @@ class @Problem
|
||||
@el.find('.capa_alert').remove()
|
||||
alert_elem = "<div class='capa_alert'>" + msg + "</div>"
|
||||
@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) =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
common/lib/xmodule/xmodule/modulestore/tests/__init__.py
Normal file
12
common/lib/xmodule/xmodule/modulestore/tests/__init__.py
Normal file
@@ -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'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
16
common/lib/xmodule/xmodule/modulestore/tests/test_xml.py
Normal file
16
common/lib/xmodule/xmodule/modulestore/tests/test_xml.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = """<html name="Voltage Source Answer" >A voltage source is non-linear!
|
||||
<div align="center">
|
||||
<img src="/static/images/circuits/voltage-source.png"/>
|
||||
\(V=V_C\)
|
||||
</div>
|
||||
But it is <a href="http://mathworld.wolfram.com/AffineFunction.html">affine</a>,
|
||||
which means linear except for an offset.
|
||||
</html>
|
||||
"""
|
||||
|
||||
html = """<html>A voltage source is non-linear!
|
||||
<div align="center">
|
||||
|
||||
</div>
|
||||
But it is <a href="http://mathworld.wolfram.com/AffineFunction.html">affine</a>,
|
||||
which means linear except for an offset.
|
||||
</html>
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<sequential>
|
||||
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" slug="Welcome" format="Video" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Welcome"/>
|
||||
<video url_name="welcome"/>
|
||||
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
|
||||
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
|
||||
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
|
||||
1
common/test/data/full/video/welcome.xml
Normal file
1
common/test/data/full/video/welcome.xml
Normal file
@@ -0,0 +1 @@
|
||||
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome"/>
|
||||
@@ -20,8 +20,8 @@ def index(request):
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
|
||||
from external_auth.views import edXauth_ssl_login
|
||||
return edXauth_ssl_login(request)
|
||||
from external_auth.views import ssl_login
|
||||
return ssl_login(request)
|
||||
|
||||
university = branding.get_university(request.META.get('HTTP_HOST'))
|
||||
if university is None:
|
||||
|
||||
@@ -321,7 +321,7 @@ def _has_staff_access_to_location(user, location):
|
||||
return True
|
||||
|
||||
# If not global staff, is the user in the Auth group for this class?
|
||||
user_groups = [x[1] for x in user.groups.values_list()]
|
||||
user_groups = [g.name for g in user.groups.all()]
|
||||
staff_group = _course_staff_group_name(location)
|
||||
if staff_group in user_groups:
|
||||
debug("Allow: user in group %s", staff_group)
|
||||
|
||||
@@ -57,7 +57,6 @@ def import_with_checks(course_dir, verbose=True):
|
||||
# module.
|
||||
modulestore = XMLModuleStore(data_dir,
|
||||
default_class=None,
|
||||
eager=True,
|
||||
course_dirs=course_dirs)
|
||||
|
||||
def str_of_err(tpl):
|
||||
|
||||
@@ -23,7 +23,6 @@ def import_course(course_dir, verbose=True):
|
||||
# module.
|
||||
modulestore = XMLModuleStore(data_dir,
|
||||
default_class=None,
|
||||
eager=True,
|
||||
course_dirs=course_dirs)
|
||||
|
||||
def str_of_err(tpl):
|
||||
|
||||
@@ -195,7 +195,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else None
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
@@ -254,7 +253,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
node_path=settings.NODE_PATH,
|
||||
anonymous_student_id=anonymous_student_id
|
||||
anonymous_student_id=anonymous_student_id,
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
|
||||
@@ -68,7 +68,6 @@ def xml_store_config(data_dir):
|
||||
'OPTIONS': {
|
||||
'data_dir': data_dir,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'eager': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +203,8 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.assertEqual(len(courses), 1)
|
||||
course = courses[0]
|
||||
self.enroll(course)
|
||||
|
||||
course_id = course.id
|
||||
|
||||
n = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
@@ -214,7 +214,8 @@ class PageLoader(ActivateLoginTestCase):
|
||||
print "Checking ", descriptor.location.url()
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'location': descriptor.location.url()}))
|
||||
kwargs={'course_id': course_id,
|
||||
'location': descriptor.location.url()}))
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
|
||||
@@ -5,8 +5,6 @@ import itertools
|
||||
|
||||
from functools import partial
|
||||
|
||||
from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -152,7 +150,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
course_id, request.user, section_descriptor)
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache, course_id)
|
||||
student_module_cache, course_id, position)
|
||||
if module is None:
|
||||
# User is probably being clever and trying to access something
|
||||
# they don't have access to.
|
||||
@@ -196,7 +194,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def jump_to(request, location):
|
||||
def jump_to(request, course_id, location):
|
||||
'''
|
||||
Show the page that contains a specific location.
|
||||
|
||||
@@ -213,15 +211,18 @@ def jump_to(request, location):
|
||||
|
||||
# Complain if there's not data for this location
|
||||
try:
|
||||
(course_id, chapter, section, position) = path_to_location(modulestore(), location)
|
||||
(course_id, chapter, section, position) = path_to_location(modulestore(), course_id, location)
|
||||
except ItemNotFoundError:
|
||||
raise Http404("No data at this location: {0}".format(location))
|
||||
except NoPathToItem:
|
||||
raise Http404("This location is not in any class: {0}".format(location))
|
||||
|
||||
# Rely on index to do all error handling and access control.
|
||||
return index(request, course_id, chapter, section, position)
|
||||
|
||||
return redirect('courseware_position',
|
||||
course_id=course_id,
|
||||
chapter=chapter,
|
||||
section=section,
|
||||
position=position)
|
||||
@ensure_csrf_cookie
|
||||
def course_info(request, course_id):
|
||||
"""
|
||||
@@ -328,6 +329,10 @@ def progress(request, course_id, student_id=None):
|
||||
# NOTE: To make sure impersonation by instructor works, use
|
||||
# student instead of request.user in the rest of the function.
|
||||
|
||||
# The pre-fetching of groups is done to make auth checks not require an
|
||||
# additional DB lookup (this kills the Progress page in particular).
|
||||
student = User.objects.prefetch_related("groups").get(id=student.id)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course_id, student, course)
|
||||
course_module = get_module(student, request, course.location,
|
||||
|
||||
@@ -149,8 +149,8 @@ class HtmlResponse(HttpResponse):
|
||||
def __init__(self, html=''):
|
||||
super(HtmlResponse, self).__init__(html, content_type='text/plain')
|
||||
|
||||
class ViewNameMiddleware(object):
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
class ViewNameMiddleware(object):
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
request.view_name = view_func.__name__
|
||||
|
||||
class QueryCountDebugMiddleware(object):
|
||||
|
||||
@@ -77,7 +77,7 @@ MITX_FEATURES = {
|
||||
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
|
||||
'AUTH_USE_OPENID': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES' : False,
|
||||
|
||||
'AUTH_USE_OPENID_PROVIDER': False,
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor",
|
||||
]
|
||||
NODE_PATH = ':'.join(node_paths)
|
||||
|
||||
|
||||
############################ OpenID Provider ##################################
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net']
|
||||
|
||||
################################## MITXWEB #####################################
|
||||
# This is where we stick our compiled template files. Most of the app uses Mako
|
||||
# templates
|
||||
@@ -219,7 +223,6 @@ MODULESTORE = {
|
||||
'OPTIONS': {
|
||||
'data_dir': DATA_DIR,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'eager': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
|
||||
|
||||
################################ OpenID Auth #################################
|
||||
MITX_FEATURES['AUTH_USE_OPENID'] = True
|
||||
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
|
||||
MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
|
||||
|
||||
INSTALLED_APPS += ('external_auth',)
|
||||
@@ -115,6 +116,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints
|
||||
OPENID_USE_AS_ADMIN_LOGIN = False
|
||||
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
|
||||
|
||||
################################ MIT Certificates SSL Auth #################################
|
||||
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
|
||||
|
||||
|
||||
@@ -123,6 +123,11 @@ CACHES = {
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
################################## OPENID ######################################
|
||||
MITX_FEATURES['AUTH_USE_OPENID'] = True
|
||||
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
|
||||
|
||||
############################ FILE UPLOADS (ASKBOT) #############################
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = TEST_ROOT / "uploads"
|
||||
|
||||
@@ -115,6 +115,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover, &:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
@include button(shiny, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
float: left;
|
||||
font: normal 1.2rem/1.6rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 10px 0px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
width: flex-grid(3, 8);
|
||||
|
||||
&:hover {
|
||||
color: rgb(255,255,255);
|
||||
}
|
||||
}
|
||||
|
||||
span.register {
|
||||
background: lighten($blue, 20%);
|
||||
border: 1px solid $blue;
|
||||
@@ -125,7 +149,10 @@
|
||||
padding: 10px 0px 8px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
width: flex-grid(12);
|
||||
float: left;
|
||||
margin: 1px flex-gutter(8) 0 0;
|
||||
@include transition();
|
||||
width: flex-grid(5, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
padding: 12px 0px;
|
||||
width: 100%;
|
||||
|
||||
a.university {
|
||||
.university {
|
||||
background: rgba(255,255,255, 1);
|
||||
border: 1px solid rgb(180,180,180);
|
||||
@include border-radius(3px);
|
||||
@@ -269,17 +269,14 @@
|
||||
color: $lighter-base-font-color;
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-family: $sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
@include inline-block;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 0;
|
||||
padding: 5px 10px;
|
||||
|
||||
float: left;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -306,8 +303,12 @@
|
||||
background: $yellow;
|
||||
border: 1px solid rgb(200,200,200);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
margin-top: 16px;
|
||||
margin-top: 17px;
|
||||
margin-right: flex-gutter();
|
||||
padding: 5px;
|
||||
width: flex-grid(8);
|
||||
float: left;
|
||||
@include box-sizing(border-box);
|
||||
|
||||
p {
|
||||
color: $lighter-base-font-color;
|
||||
@@ -317,93 +318,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
@include clearfix;
|
||||
margin-top: 22px;
|
||||
position: relative;
|
||||
@include transition(opacity, 0.15s, linear);
|
||||
width: 100%;
|
||||
|
||||
|
||||
.course-work-icon {
|
||||
@include background-image(url('../images/portal-icons/pencil-icon.png'));
|
||||
background-size: cover;
|
||||
float: left;
|
||||
height: 22px;
|
||||
opacity: 0.7;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.complete {
|
||||
float: right;
|
||||
|
||||
p {
|
||||
color: $lighter-base-font-color;
|
||||
font-style: italic;
|
||||
@include inline-block;
|
||||
text-align: right;
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.6);
|
||||
|
||||
.completeness {
|
||||
color: $base-font-color;
|
||||
font-weight: 700;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
left: 35px;
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
|
||||
.meter {
|
||||
background: rgb(245,245,245);
|
||||
border: 1px solid rgb(160,160,160);
|
||||
@include box-shadow(inset 0 0 3px 0 rgba(0,0,0, 0.15));
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(4px);
|
||||
height: 22px;
|
||||
margin: 0 auto;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
.meter-fill {
|
||||
background: $blue;
|
||||
@include background-image(linear-gradient(-45deg, rgba(255,255,255, 0.15) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(255,255,255, 0.15) 50%,
|
||||
rgba(255,255,255, 0.15) 75%,
|
||||
transparent 75%));
|
||||
background-size: 40px 40px;
|
||||
background-repeat: repeat-x;
|
||||
border: 1px solid rgb(115,115,115);
|
||||
@include border-radius(4px);
|
||||
@include box-sizing(border-box);
|
||||
content: "";
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.enter-course {
|
||||
@include button(shiny, $blue);
|
||||
@include box-sizing(border-box);
|
||||
@include border-radius(3px);
|
||||
display: block;
|
||||
float: left;
|
||||
font: normal 1rem/1.6rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 6px 0px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
width: flex-grid(4);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> a:hover {
|
||||
.cover {
|
||||
.shade {
|
||||
background: rgba(255,255,255, 0.1);
|
||||
@include background-image(linear-gradient(-90deg, rgba(255,255,255, 0.3) 0%,
|
||||
rgba(0,0,0, 0.3) 100%));
|
||||
rgba(0,0,0, 0.3) 100%));
|
||||
}
|
||||
|
||||
.arrow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background: darken(rgb(250,250,250), 5%);
|
||||
@include background-image(linear-gradient(-90deg, darken(rgb(253,253,253), 3%), darken(rgb(240,240,240), 5%)));
|
||||
border-color: darken(rgb(190,190,190), 10%);
|
||||
|
||||
.course-status {
|
||||
background: darken($yellow, 3%);
|
||||
border-color: darken(rgb(200,200,200), 3%);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,5 +374,6 @@
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,30 +72,24 @@
|
||||
else:
|
||||
course_target = reverse('about_course', args=[course.id])
|
||||
%>
|
||||
<a href="${course_target}" class="cover" style="background-image: url('${course_image_url(course)}')">
|
||||
<div class="shade"></div>
|
||||
<div class="arrow">❯</div>
|
||||
|
||||
<a href="${course_target}">
|
||||
<section class="cover" style="background-image: url('${course_image_url(course)}')">
|
||||
<div class="shade"></div>
|
||||
<div class="arrow">❯</div>
|
||||
</section>
|
||||
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
|
||||
<h3>${course.number} ${course.title}</h3>
|
||||
</hgroup>
|
||||
<section class="course-status">
|
||||
<p>Class Starts - <span>${course.start_date_text}</span></p>
|
||||
</section>
|
||||
<p class="enter-course">View Courseware</p>
|
||||
</section>
|
||||
</a>
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
<a href="${reverse('university_profile', args=[course.org])}" class="university">${get_course_about_section(course, 'university')}</a>
|
||||
<h3><a href="${course_target}">${course.number} ${course.title}</a></h3>
|
||||
</hgroup>
|
||||
<section class="course-status">
|
||||
<p>Class Starts - <span>${course.start_date_text}</span></div>
|
||||
</section>
|
||||
<section class="meta">
|
||||
<div class="course-work-icon"></div>
|
||||
<div class="progress">
|
||||
<div class="meter">
|
||||
<div class="meter-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="complete">
|
||||
##<p><span class="completeness">60%</span> complete</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</article>
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
|
||||
|
||||
|
||||
10
lms/templates/identity.xml
Normal file
10
lms/templates/identity.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
|
||||
<XRD>
|
||||
<Service priority="0">
|
||||
<Type>http://specs.openid.net/auth/2.0/signon</Type>
|
||||
<Type>http://openid.net/signon/1.1</Type>
|
||||
<URI>${url}</URI>
|
||||
</Service>
|
||||
</XRD>
|
||||
</xrds:XRDS>
|
||||
@@ -74,7 +74,7 @@
|
||||
%if show_link:
|
||||
<a href="${course_target}">
|
||||
%endif
|
||||
<span class="register disabled">You are registered for this course (${course.number}).</span>
|
||||
<span class="register disabled">You are registered for this course (${course.number})</span> <strong>View Courseware</strong>
|
||||
%if show_link:
|
||||
</a>
|
||||
%endif
|
||||
|
||||
@@ -32,3 +32,27 @@
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(function(){
|
||||
// this should be brought back into problems
|
||||
$('.longform').hide();
|
||||
$('.shortform').append('<a href="#" class="full">See full output</a>');
|
||||
|
||||
$('.full').click(function() {
|
||||
$(this).parent().siblings().slideToggle();
|
||||
$(this).parent().parent().toggleClass('open');
|
||||
var text = $(this).text() == 'See full output' ? 'Hide output' : 'See full output';
|
||||
$(this).text(text);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.collapsible section').hide()
|
||||
$('.collapsible header a').click(function() {
|
||||
$(this).parent().siblings().slideToggle();
|
||||
$(this).parent().parent().toggleClass('open');
|
||||
return false
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
52
lms/templates/provider_login.html
Normal file
52
lms/templates/provider_login.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<%inherit file="main.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<style type="text/css">
|
||||
.openid-login {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin: 100px auto;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.openid-login input[type=submit] {
|
||||
white-space: normal;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#lean_overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 100;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
</%block>
|
||||
|
||||
<section id="login-modal" class="modal login-modal openid-login">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Log In</h2>
|
||||
<hr>
|
||||
</header>
|
||||
<form id="login_form" class="login_form" method="post" action="/openid/provider/login/">
|
||||
%if error:
|
||||
<div id="login_error" class="modal-form-error" style="display: block;">Email or password is incorrect.</div>
|
||||
%endif
|
||||
<label>E-mail</label>
|
||||
<input type="text" name="email" placeholder="E-mail" tabindex="1" />
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" placeholder="Password" tabindex="2" />
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Access My Courses and Return To ${return_to}" tabindex="3" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<div id="lean_overlay"></div>
|
||||
11
lms/templates/xrds.xml
Normal file
11
lms/templates/xrds.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
|
||||
<XRD>
|
||||
<Service priority="0">
|
||||
<Type>http://specs.openid.net/auth/2.0/server</Type>
|
||||
<Type>http://openid.net/sreg/1.0</Type>
|
||||
<Type>http://openid.net/srv/ax/1.0</Type>
|
||||
<URI>${url}</URI>
|
||||
</Service>
|
||||
</XRD>
|
||||
</xrds:XRDS>
|
||||
19
lms/urls.py
19
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<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/jump_to/(?P<location>.*)$',
|
||||
'courseware.views.jump_to', name="jump_to"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'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<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
|
||||
'courseware.views.index', name="courseware_section"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/(?P<position>[^/]*)/?$',
|
||||
'courseware.views.index', name="courseware_position"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/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<course_id>[^/]+/[^/]+/[^/]+)/news$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
|
||||
'courseware.views.news', name="news"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/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 += (
|
||||
|
||||
Reference in New Issue
Block a user