From 102d4906d0d1c94aec6d2854e9e46c6707c0d09e Mon Sep 17 00:00:00 2001
From: ichuang
Date: Wed, 1 Aug 2012 22:41:27 -0400
Subject: [PATCH 05/22] fix problem with add_histogram (expects module as
second argument)
---
lms/djangoapps/courseware/module_render.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 80a4ef90fc..16921d1d50 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -184,7 +184,7 @@ def get_module(user, request, location, student_module_cache, position=None):
)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
- module.get_html = add_histogram(module.get_html)
+ module.get_html = add_histogram(module.get_html, module)
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.
From a759850e3e45a04e5953320fdb001e98df6a81be Mon Sep 17 00:00:00 2001
From: ichuang
Date: Wed, 1 Aug 2012 22:42:06 -0400
Subject: [PATCH 06/22] add SSL / MIT certificates auth; clean up
external_auth.views
---
common/djangoapps/external_auth/views.py | 162 +++++++++----
common/djangoapps/student/views.py | 4 +
lms/djangoapps/ssl_auth/__init__.py | 0
lms/djangoapps/ssl_auth/ssl_auth.py | 290 -----------------------
lms/envs/dev.py | 3 +
5 files changed, 124 insertions(+), 335 deletions(-)
delete mode 100644 lms/djangoapps/ssl_auth/__init__.py
delete mode 100755 lms/djangoapps/ssl_auth/ssl_auth.py
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 57131f21da..5004d614d5 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -1,8 +1,7 @@
-# from pprint import pprint
-
import json
import logging
import random
+import re
import string
from external_auth.models import ExternalAuthMap
@@ -25,7 +24,6 @@ except ImportError:
from django_future.csrf import ensure_csrf_cookie
from util.cache import cache_if_anonymous
-#from django_openid_auth import views as openid_views
from django_openid_auth import auth as openid_auth
from openid.consumer.consumer import (Consumer, SUCCESS, CANCEL, FAILURE)
import django_openid_auth
@@ -43,6 +41,12 @@ def default_render_failure(request, message, status=403, template_name='extauth_
data = render_to_string( template_name, dict(message=message, exception=exception))
return HttpResponse(data, status=status)
+#-----------------------------------------------------------------------------
+# Openid
+
+def GenPasswd(length=12, chars=string.letters + string.digits):
+ return ''.join([random.choice(chars) for i in range(length)])
+
@csrf_exempt
def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME, render_failure=None):
"""Complete the openid login process"""
@@ -63,50 +67,62 @@ def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_N
log.debug('openid success, details=%s' % details)
- # see if we have a map from this external_id to an edX username
- try:
- eamap = ExternalAuthMap.objects.get(external_id=external_id)
- log.debug('Found eamap=%s' % eamap)
- except ExternalAuthMap.DoesNotExist:
- # go render form for creating edX user
- eamap = ExternalAuthMap(external_id = external_id,
- external_domain = "openid:%s" % settings.OPENID_SSO_SERVER_URL,
- external_credentials = json.dumps(details),
- )
- eamap.external_email = details.get('email','')
- eamap.external_name = '%s %s' % (details.get('first_name',''),details.get('last_name',''))
-
- def GenPasswd(length=12, chars=string.letters + string.digits):
- return ''.join([random.choice(chars) for i in range(length)])
- eamap.internal_password = GenPasswd()
- 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)
-
- 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)
-
- if not user.is_active:
- log.warning("External Auth: 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')
-
- login(request, user)
- request.session.set_expiry(0)
- student_views.try_change_enrollment(request)
- log.info("Login success - {0} ({1})".format(user.username, user.email))
- return redirect('/')
-
+ 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',''))
+ )
+
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):
+ # see if we have a map from this external_id to an edX username
+ try:
+ eamap = ExternalAuthMap.objects.get(external_id=external_id)
+ 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.external_email = email
+ eamap.external_name = fullname
+ eamap.internal_password = GenPasswd()
+ 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)
+ 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)
+
+ if not user.is_active:
+ log.warning("External Auth: 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')
+
+ login(request, user)
+ request.session.set_expiry(0)
+ student_views.try_change_enrollment(request)
+ log.info("Login success - {0} ({1})".format(user.username, user.email))
+ return redirect('/')
+
+#-----------------------------------------------------------------------------
+# generic external auth signup
+
@ensure_csrf_cookie
@cache_if_anonymous
def edXauth_signup(request, eamap=None):
@@ -135,3 +151,59 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email)
return student_views.main_index(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.
+ '''
+ ss = re.search('/emailAddress=(.*)@([^/]+)', dn)
+ if ss:
+ user = ss.group(1)
+ email = "%s@%s" % (user, ss.group(2))
+ else:
+ return None
+ ss = re.search('/CN=([^/]+)/', dn)
+ if ss:
+ fullname = ss.group(1)
+ else:
+ return None
+ return (user, email, fullname)
+
+@csrf_exempt
+def edXauth_ssl_login(request):
+ """
+ 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.
+
+ 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.main_index, and no authentication.
+ """
+ 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,'')
+ if not cert:
+ cert = request._req.subprocess_env.get(certkey,'') # try the direct apache2 SSL key
+ if not cert:
+ # no certificate information - go onward to main index
+ return student_views.main_index()
+
+ (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)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 3ba83f42bb..7937d67980 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -60,6 +60,10 @@ def index(request):
if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
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)
+
return main_index()
def main_index(extra_context = {}):
diff --git a/lms/djangoapps/ssl_auth/__init__.py b/lms/djangoapps/ssl_auth/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/lms/djangoapps/ssl_auth/ssl_auth.py b/lms/djangoapps/ssl_auth/ssl_auth.py
deleted file mode 100755
index adbb2bf94d..0000000000
--- a/lms/djangoapps/ssl_auth/ssl_auth.py
+++ /dev/null
@@ -1,290 +0,0 @@
-"""
-User authentication backend for ssl (no pw required)
-"""
-
-from django.conf import settings
-from django.contrib import auth
-from django.contrib.auth.models import User, check_password
-from django.contrib.auth.backends import ModelBackend
-from django.contrib.auth.middleware import RemoteUserMiddleware
-from django.core.exceptions import ImproperlyConfigured
-import os
-import string
-import re
-from random import choice
-
-from student.models import UserProfile
-
-#-----------------------------------------------------------------------------
-
-
-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.
- '''
- ss = re.search('/emailAddress=(.*)@([^/]+)', dn)
- if ss:
- user = ss.group(1)
- email = "%s@%s" % (user, ss.group(2))
- else:
- return None
- ss = re.search('/CN=([^/]+)/', dn)
- if ss:
- fullname = ss.group(1)
- else:
- return None
- return (user, email, fullname)
-
-
-def check_nginx_proxy(request):
- '''
- Check for keys in the HTTP header (META) to se if we are behind an ngix reverse proxy.
- If so, get user info from the SSL DN string and return that, as (user,email,fullname)
- '''
- m = request.META
- if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth
- if not m.has_key('HTTP_SSL_CLIENT_S_DN'):
- return None
- dn = m['HTTP_SSL_CLIENT_S_DN']
- return ssl_dn_extract_info(dn)
- return None
-
-#-----------------------------------------------------------------------------
-
-
-def get_ssl_username(request):
- x = check_nginx_proxy(request)
- if x:
- return x[0]
- env = request._req.subprocess_env
- if env.has_key('SSL_CLIENT_S_DN_Email'):
- email = env['SSL_CLIENT_S_DN_Email']
- user = email[:email.index('@')]
- return user
- return None
-
-#-----------------------------------------------------------------------------
-
-
-class NginxProxyHeaderMiddleware(RemoteUserMiddleware):
- '''
- Django "middleware" function for extracting user information from HTTP request.
-
- '''
- # this field is generated by nginx's reverse proxy
- header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use
-
- def process_request(self, request):
- # AuthenticationMiddleware is required so that request.user exists.
- if not hasattr(request, 'user'):
- raise ImproperlyConfigured(
- "The Django remote user auth middleware requires the"
- " authentication middleware to be installed. Edit your"
- " MIDDLEWARE_CLASSES setting to insert"
- " 'django.contrib.auth.middleware.AuthenticationMiddleware'"
- " before the RemoteUserMiddleware class.")
-
- #raise ImproperlyConfigured('[ProxyHeaderMiddleware] request.META=%s' % repr(request.META))
-
- try:
- username = request.META[self.header] # try the nginx META key first
- except KeyError:
- try:
- env = request._req.subprocess_env # else try the direct apache2 SSL key
- if env.has_key('SSL_CLIENT_S_DN'):
- username = env['SSL_CLIENT_S_DN']
- else:
- raise ImproperlyConfigured('no ssl key, env=%s' % repr(env))
- username = ''
- except:
- # If specified header doesn't exist then return (leaving
- # request.user set to AnonymousUser by the
- # AuthenticationMiddleware).
- return
- # If the user is already authenticated and that user is the user we are
- # getting passed in the headers, then the correct user is already
- # persisted in the session and we don't need to continue.
-
- #raise ImproperlyConfigured('[ProxyHeaderMiddleware] username=%s' % username)
-
- if request.user.is_authenticated():
- if request.user.username == self.clean_username(username, request):
- #raise ImproperlyConfigured('%s already authenticated (%s)' % (username,request.user.username))
- return
- # We are seeing this user for the first time in this session, attempt
- # to authenticate the user.
- #raise ImproperlyConfigured('calling auth.authenticate, remote_user=%s' % username)
- user = auth.authenticate(remote_user=username)
- if user:
- # User is valid. Set request.user and persist user in the session
- # by logging the user in.
- request.user = user
- if settings.DEBUG: print "[ssl_auth.ssl_auth.NginxProxyHeaderMiddleware] logging in user=%s" % user
- auth.login(request, user)
-
- def clean_username(self, username, request):
- '''
- username is the SSL DN string - extract the actual username from it and return
- '''
- info = ssl_dn_extract_info(username)
- if not info:
- return None
- (username, email, fullname) = info
- return username
-
-#-----------------------------------------------------------------------------
-
-
-class SSLLoginBackend(ModelBackend):
- '''
- Django authentication back-end which auto-logs-in a user based on having
- already authenticated with an MIT certificate (SSL).
- '''
- def authenticate(self, username=None, password=None, remote_user=None):
-
- # remote_user is from the SSL_DN string. It will be non-empty only when
- # the user has already passed the server authentication, which means
- # matching with the certificate authority.
- if not remote_user:
- # no remote_user, so check username (but don't auto-create user)
- if not username:
- return None
- return None # pass on to another authenticator backend
- #raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
- try:
- user = User.objects.get(username=username) # if user already exists don't create it
- return user
- except User.DoesNotExist:
- return None
- return None
-
- #raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
- #if not os.environ.has_key('HTTPS'):
- # return None
- #if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
- # return None
-
- def GenPasswd(length=8, chars=string.letters + string.digits):
- return ''.join([choice(chars) for i in range(length)])
-
- # convert remote_user to user, email, fullname
- info = ssl_dn_extract_info(remote_user)
- #raise ImproperlyConfigured("[SSLLoginBackend] looking up %s" % repr(info))
- if not info:
- #raise ImproperlyConfigured("[SSLLoginBackend] remote_user=%s, info=%s" % (remote_user,info))
- return None
- (username, email, fullname) = info
-
- try:
- user = User.objects.get(username=username) # if user already exists don't create it
- except User.DoesNotExist:
- if not settings.DEBUG:
- raise "User does not exist. Not creating user; potential schema consistency issues"
- #raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info))
- user = User(username=username, password=GenPasswd()) # create new User
- user.is_staff = False
- user.is_superuser = False
- # get first, last name from fullname
- name = fullname
- if not name.count(' '):
- user.first_name = " "
- user.last_name = name
- mn = ''
- else:
- user.first_name = name[:name.find(' ')]
- ml = name[name.find(' '):].strip()
- if ml.count(' '):
- user.last_name = ml[ml.rfind(' '):]
- mn = ml[:ml.rfind(' ')]
- else:
- user.last_name = ml
- mn = ''
- # set email
- user.email = email
- # cleanup last name
- user.last_name = user.last_name.strip()
- # save
- user.save()
-
- # auto-create user profile
- up = UserProfile(user=user)
- up.name = fullname
- up.save()
-
- #tui = user.get_profile()
- #tui.middle_name = mn
- #tui.role = 'Misc'
- #tui.section = None # no section assigned at first
- #tui.save()
- # return None
- return user
-
- def get_user(self, user_id):
- #if not os.environ.has_key('HTTPS'):
- # return None
- #if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
- # return None
- try:
- return User.objects.get(pk=user_id)
- except User.DoesNotExist:
- return None
-
-#-----------------------------------------------------------------------------
-# OLD!
-
-
-class AutoLoginBackend:
- def authenticate(self, username=None, password=None):
- raise ImproperlyConfigured("in AutoLoginBackend, username=%s" % username)
- if not os.environ.has_key('HTTPS'):
- return None
- if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on
- return None
-
- def GenPasswd(length=8, chars=string.letters + string.digits):
- return ''.join([choice(chars) for i in range(length)])
-
- try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
- user = User(username=username, password=GenPasswd())
- user.is_staff = False
- user.is_superuser = False
- # get first, last name
- name = os.environ.get('SSL_CLIENT_S_DN_CN').strip()
- if not name.count(' '):
- user.first_name = " "
- user.last_name = name
- mn = ''
- else:
- user.first_name = name[:name.find(' ')]
- ml = name[name.find(' '):].strip()
- if ml.count(' '):
- user.last_name = ml[ml.rfind(' '):]
- mn = ml[:ml.rfind(' ')]
- else:
- user.last_name = ml
- mn = ''
- # get email
- user.email = os.environ.get('SSL_CLIENT_S_DN_Email')
- # save
- user.save()
- tui = user.get_profile()
- tui.middle_name = mn
- tui.role = 'Misc'
- tui.section = None# no section assigned at first
- tui.save()
- # return None
- return user
-
- def get_user(self, user_id):
- if not os.environ.has_key('HTTPS'):
- return None
- if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on
- return None
- try:
- return User.objects.get(pk=user_id)
- except User.DoesNotExist:
- return None
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index f9b7ba10a0..83bc596f1e 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -67,6 +67,9 @@ 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
+################################ MIT Certificates SSL Auth #################################
+MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
+
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
From 4a0d0a08db20fe64fa0dceb08d157cce1cfaa026 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Wed, 1 Aug 2012 23:37:35 -0400
Subject: [PATCH 07/22] minor change so that SSL code doesn't interfere with
non-nginx instances
---
common/djangoapps/external_auth/views.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 5004d614d5..41062735d4 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -194,7 +194,10 @@ def edXauth_ssl_login(request):
if not cert:
cert = request.META.get('HTTP_'+certkey,'')
if not cert:
- cert = request._req.subprocess_env.get(certkey,'') # try the direct apache2 SSL key
+ try:
+ cert = request._req.subprocess_env.get(certkey,'') # try the direct apache2 SSL key
+ except Exception as err:
+ pass
if not cert:
# no certificate information - go onward to main index
return student_views.main_index()
From 727e51411f7e096c92745c3deb630bd77b2f119c Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 08:59:02 -0400
Subject: [PATCH 08/22] small change so that ssl authenticated user can logout
to see main screen
---
common/djangoapps/external_auth/views.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 41062735d4..55ff4b4194 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -80,7 +80,8 @@ def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_N
#-----------------------------------------------------------------------------
# generic external auth login or signup
-def edXauth_external_login_or_signup(request, external_id, external_domain, credentials, email, fullname):
+def edXauth_external_login_or_signup(request, external_id, external_domain, credentials, email, fullname,
+ retfun=None):
# see if we have a map from this external_id to an edX username
try:
eamap = ExternalAuthMap.objects.get(external_id=external_id)
@@ -118,7 +119,10 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred
request.session.set_expiry(0)
student_views.try_change_enrollment(request)
log.info("Login success - {0} ({1})".format(user.username, user.email))
- return redirect('/')
+ if retfun is None:
+ return redirect('/')
+ return retfun()
+
#-----------------------------------------------------------------------------
# generic external auth signup
@@ -209,4 +213,5 @@ def edXauth_ssl_login(request):
external_domain="ssl:MIT",
credentials=cert,
email=email,
- fullname=fullname)
+ fullname=fullname,
+ retfun = student_views.main_index)
From 23c3c5a6529db587e505aed01f908ea6684e3b8c Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 09:37:24 -0400
Subject: [PATCH 09/22] print -> log.debug, rename function from camel case
---
common/djangoapps/external_auth/views.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 55ff4b4194..fdffaf0c1d 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -35,16 +35,16 @@ log = logging.getLogger("mitx.external_auth")
@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
- print "in openid_failure: "
+ log.debug(message)
data = render_to_string( template_name, dict(message=message, exception=exception))
return HttpResponse(data, status=status)
#-----------------------------------------------------------------------------
# Openid
-def GenPasswd(length=12, chars=string.letters + string.digits):
+def edXauth_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)])
@csrf_exempt
@@ -94,7 +94,7 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred
)
eamap.external_email = email
eamap.external_name = fullname
- eamap.internal_password = GenPasswd()
+ eamap.internal_password = edXauth_generate_password()
log.debug('created eamap=%s' % eamap)
eamap.save()
From b2e9d980ff4840539fd593f43c175f1040221d3b Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 09:42:26 -0400
Subject: [PATCH 10/22] don't overwrite oid_backend
---
common/djangoapps/external_auth/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index fdffaf0c1d..827f2bc4c5 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -63,7 +63,7 @@ def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_N
if openid_response.status == SUCCESS:
external_id = openid_response.identity_url
oid_backend = openid_auth.OpenIDBackend()
- details = oid_backened = oid_backend._extract_user_details(openid_response)
+ details = oid_backend._extract_user_details(openid_response)
log.debug('openid success, details=%s' % details)
From f2a9110bdaa804bf20a91d9020113f2657d05c76 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 09:56:33 -0400
Subject: [PATCH 11/22] change model to have external_id and external_domain be
unique_together
---
common/djangoapps/external_auth/models.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/external_auth/models.py b/common/djangoapps/external_auth/models.py
index b8b167d25d..e43b306bbb 100644
--- a/common/djangoapps/external_auth/models.py
+++ b/common/djangoapps/external_auth/models.py
@@ -13,7 +13,9 @@ from django.db import models
from django.contrib.auth.models import User
class ExternalAuthMap(models.Model):
- external_id = models.CharField(max_length=255, db_index=True, unique=True)
+ class Meta:
+ unique_together = (('external_id', 'external_domain'), )
+ external_id = models.CharField(max_length=255, db_index=True)
external_domain = models.CharField(max_length=255, db_index=True)
external_credentials = models.TextField(blank=True) # JSON dictionary
external_email = models.CharField(max_length=255, db_index=True)
From 613c53a7109b9162acc439e202b3430f099ee35f Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 10:05:26 -0400
Subject: [PATCH 12/22] slight cleanup, no need to import all of
django_openid_auth
---
common/djangoapps/external_auth/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 827f2bc4c5..740b4ed1ac 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -26,7 +26,7 @@ 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
+import django_openid_auth.views as openid_views
import student.views as student_views
@@ -56,7 +56,7 @@ def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_N
getattr(settings, 'OPENID_RENDER_FAILURE', None) or \
default_render_failure
- openid_response = django_openid_auth.views.parse_openid_response(request)
+ openid_response = openid_views.parse_openid_response(request)
if not openid_response:
return render_failure(request, 'This is an OpenID relying party endpoint.')
From 3eff9ffecd2382ba0bad9b877376388daffa7a8e Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 13:28:52 -0400
Subject: [PATCH 13/22] match external_domain as well when retrieving
ExternalAuthMap objects
---
common/djangoapps/external_auth/views.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 740b4ed1ac..d00a0a7182 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -84,7 +84,9 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred
retfun=None):
# see if we have a map from this external_id to an edX username
try:
- eamap = ExternalAuthMap.objects.get(external_id=external_id)
+ 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
From a7103ff8932ddfdcd10e844aa71fd3901690de47 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 13:39:12 -0400
Subject: [PATCH 14/22] switch to PascalCase, remove unnecessary assignment
---
common/djangoapps/student/views.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 7937d67980..35ce225011 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -280,14 +280,14 @@ def create_account(request, post_override=None):
# if doing signup for an external authorization, then get email, password, name from the eamap
# don't use the ones from the form, since the user could have hacked those
- doExternalAuth = 'ExternalAuthMap' in request.session
- if doExternalAuth:
+ DoExternalAuth = 'ExternalAuthMap' in request.session
+ if DoExternalAuth:
eamap = request.session['ExternalAuthMap']
email = eamap.external_email
name = eamap.external_name
password = eamap.internal_password
post_vars = dict(post_vars.items())
- post_vars.update(dict(email=email, name=name, password=password, username=post_vars['username']))
+ post_vars.update(dict(email=email, name=name, password=password))
log.debug('extauth test: post_vars = %s' % post_vars)
# Confirm we have a properly formed request
@@ -411,7 +411,7 @@ def create_account(request, post_override=None):
try_change_enrollment(request)
- if doExternalAuth:
+ if DoExternalAuth:
eamap.user = login_user
eamap.dtsignup = datetime.datetime.now()
eamap.save()
From 46bc7bc499fb63005d5225a465470c9d7481aa3d Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 13:44:37 -0400
Subject: [PATCH 15/22] remove unnecessary hidden fields
---
lms/templates/signup_modal.html | 6 ------
1 file changed, 6 deletions(-)
diff --git a/lms/templates/signup_modal.html b/lms/templates/signup_modal.html
index 346027418d..1510eb407b 100644
--- a/lms/templates/signup_modal.html
+++ b/lms/templates/signup_modal.html
@@ -30,15 +30,9 @@
% else:
Welcome ${extauth_email}
-
-
Enter a public username:
-
-
-
-
% endif
From 10b2c212b6d9159acf430f8d65738471292eea5c Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 13:52:25 -0400
Subject: [PATCH 16/22] fix javascript for signup modal .click() in index.html
for safari
---
lms/templates/index.html | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/lms/templates/index.html b/lms/templates/index.html
index dcc18d4de1..4255251604 100644
--- a/lms/templates/index.html
+++ b/lms/templates/index.html
@@ -148,7 +148,26 @@
% if show_signup_immediately is not UNDEFINED:
From dff380e8076a49a82d10bda267d89e9d7259133d Mon Sep 17 00:00:00 2001
From: ichuang
Date: Thu, 2 Aug 2012 14:19:56 -0400
Subject: [PATCH 17/22] cleanup lms urls (remove cruft from debugging openid)
---
lms/urls.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/lms/urls.py b/lms/urls.py
index 8c36857ee3..35a71920a5 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -166,10 +166,6 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
url(r'^openid/complete/$', 'external_auth.views.edXauth_openid_login_complete', name='openid-complete'),
url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'),
)
- urlpatterns += (
- url(r'^extauth/$', 'external_auth.views.edXauth_signup', name='extauth-signup'),
- )
- # urlpatterns += (url(r'^openid/', include('django_openid_auth.urls')),)
urlpatterns = patterns(*urlpatterns)
From 987b9c11a94372f6d4c888755a1ce5ff5a036e30 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 2 Aug 2012 14:05:42 -0400
Subject: [PATCH 18/22] Use url_name for chapters and sections in lms views
* got rid of the hackish conversions between ' ' and '_'
* use url_name and display_name where appropriate
* update templates to match.
---
common/lib/xmodule/xmodule/x_module.py | 2 ++
lms/djangoapps/courseware/grades.py | 33 ++++++++++++++--------
lms/djangoapps/courseware/module_render.py | 28 ++++++++++--------
lms/djangoapps/courseware/views.py | 29 ++++---------------
lms/templates/accordion.html | 6 ++--
lms/templates/profile.html | 28 +++++++++---------
6 files changed, 62 insertions(+), 64 deletions(-)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index ac6b5db5a4..1d16849d67 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -339,6 +339,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
module
display_name: The name to use for displaying this module to the
user
+ url_name: The name to use for this module in urls and other places
+ where a unique name is needed.
format: The format of this module ('Homework', 'Lab', etc)
graded (bool): Whether this module is should be graded or not
start (string): The date for which this module will be available
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 5a817e3d6c..717cbde140 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -12,18 +12,23 @@ _log = logging.getLogger("mitx.courseware")
def grade_sheet(student, course, grader, student_module_cache):
"""
- This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
+ This pulls a summary of all problems in the course. It returns a dictionary
+ with two datastructures:
- - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters,
- each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded
- problems, and is good for displaying a course summary with due dates, etc.
+ - courseware_summary is a summary of all sections with problems in the
+ course. It is organized as an array of chapters, each containing an array of
+ sections, each containing an array of scores. This contains information for
+ graded and ungraded problems, and is good for displaying a course summary
+ with due dates, etc.
- - grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
+ - grade_summary is the output from the course grader. More information on
+ the format is in the docstring for CourseGrader.
Arguments:
student: A User object for the student to grade
course: An XModule containing the course to grade
- student_module_cache: A StudentModuleCache initialized with all instance_modules for the student
+ student_module_cache: A StudentModuleCache initialized with all
+ instance_modules for the student
"""
totaled_scores = {}
chapters = []
@@ -51,12 +56,16 @@ def grade_sheet(student, course, grader, student_module_cache):
correct = total
if not total > 0:
- #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
+ #We simply cannot grade a problem that is 12/0, because we
+ #might need it as a percentage
graded = False
- scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
+ scores.append(Score(correct, total, graded,
+ module.metadata.get('display_name')))
+
+ section_total, graded_total = graders.aggregate_scores(
+ scores, s.metadata.get('display_name'))
- section_total, graded_total = graders.aggregate_scores(scores, s.metadata.get('display_name'))
#Add the graded total to totaled_scores
format = s.metadata.get('format', "")
if format and graded_total.possible > 0:
@@ -65,7 +74,8 @@ def grade_sheet(student, course, grader, student_module_cache):
totaled_scores[format] = format_scores
sections.append({
- 'section': s.metadata.get('display_name'),
+ 'display_name': s.metadata.get('display_name'),
+ 'url_name': s.metadata.get('url_name'),
'scores': scores,
'section_total': section_total,
'format': format,
@@ -74,7 +84,8 @@ def grade_sheet(student, course, grader, student_module_cache):
})
chapters.append({'course': course.metadata.get('display_name'),
- 'chapter': c.metadata.get('display_name'),
+ 'display_name': c.metadata.get('display_name'),
+ 'url_name': c.metadata.get('url_name'),
'sections': sections})
grade_summary = grader.grade(totaled_scores)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 91d4efa651..4699ed50a4 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -35,10 +35,12 @@ def toc_for_course(user, request, course, active_chapter, active_section):
Create a table of contents from the module store
Return format:
- [ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
+ [ {'display_name': name, 'url_name': url_name,
+ 'sections': SECTIONS, 'active': bool}, ... ]
where SECTIONS is a list
- [ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
+ [ {'display_name': name, 'url_name': url_name,
+ 'format': format, 'due': due, 'active' : bool}, ...]
active is set for the section and chapter corresponding to the passed
parameters. Everything else comes from the xml, or defaults to "".
@@ -59,12 +61,14 @@ def toc_for_course(user, request, course, active_chapter, active_section):
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc:
- sections.append({'name': section.metadata.get('display_name'),
+ sections.append({'display_name': section.metadata.get('display_name'),
+ 'url_name': section.metadata.get('url_name'),
'format': section.metadata.get('format', ''),
'due': section.metadata.get('due', ''),
'active': active})
- chapters.append({'name': chapter.metadata.get('display_name'),
+ chapters.append({'display_name': chapter.metadata.get('display_name'),
+ 'url_name': chapter.metadata.get('url_name'),
'sections': sections,
'active': chapter.metadata.get('display_name') == active_chapter})
return chapters
@@ -76,8 +80,8 @@ def get_section(course_module, chapter, section):
or None if this doesn't specify a valid section
course: Course url
- chapter: Chapter name
- section: Section name
+ chapter: Chapter url_name
+ section: Section url_name
"""
if course_module is None:
@@ -85,7 +89,7 @@ def get_section(course_module, chapter, section):
chapter_module = None
for _chapter in course_module.get_children():
- if _chapter.metadata.get('display_name') == chapter:
+ if _chapter.metadata.get('url_name') == chapter:
chapter_module = _chapter
break
@@ -94,7 +98,7 @@ def get_section(course_module, chapter, section):
section_module = None
for _section in chapter_module.get_children():
- if _section.metadata.get('display_name') == section:
+ if _section.metadata.get('url_name') == section:
section_module = _section
break
@@ -141,12 +145,12 @@ def get_module(user, request, location, student_module_cache, position=None):
# Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
- # Fully qualified callback URL for external queueing system
- xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL +
- 'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' +
+ # Fully qualified callback URL for external queueing system
+ xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL +
+ 'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' +
'score_update')
- # Default queuename is course-specific and is derived from the course that
+ # Default queuename is course-specific and is derived from the course that
# contains the current module.
# TODO: Queuename should be derived from 'course_settings.json' of each course
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 2b55b48caf..18b710e108 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -54,11 +54,6 @@ def user_groups(user):
return group_names
-def format_url_params(params):
- return [urllib.quote(string.replace(' ', '_'))
- if string is not None else None
- for string in params]
-
@ensure_csrf_cookie
@cache_if_anonymous
@@ -124,7 +119,6 @@ def profile(request, course_id, student_id=None):
'language': user_info.language,
'email': student.email,
'course': course,
- 'format_url_params': format_url_params,
'csrf': csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache))
@@ -138,9 +132,9 @@ def render_accordion(request, course, chapter, section):
If chapter and section are '' or None, renders a default accordion.
- Returns (initialization_javascript, content)'''
+ Returns the html string'''
- # TODO (cpennington): do the right thing with courses
+ # grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
active_chapter = 1
@@ -152,7 +146,6 @@ def render_accordion(request, course, chapter, section):
('toc', toc),
('course_name', course.title),
('course_id', course.id),
- ('format_url_params', format_url_params),
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
return render_to_string('accordion.html', context)
@@ -169,9 +162,9 @@ def index(request, course_id, chapter=None, section=None,
Arguments:
- request : HTTP request
- - course : coursename (str)
- - chapter : chapter name (str)
- - section : section name (str)
+ - course_id : course id (str: ORG/course/URL_NAME)
+ - chapter : chapter url_name (str)
+ - section : section url_name (str)
- position : position in module, eg of module (str)
Returns:
@@ -180,16 +173,6 @@ def index(request, course_id, chapter=None, section=None,
'''
course = check_course(course_id)
- def clean(s):
- ''' Fixes URLs -- we convert spaces to _ in URLs to prevent
- funny encoding characters and keep the URLs readable. This undoes
- that transformation.
- '''
- return s.replace('_', ' ') if s is not None else None
-
- chapter = clean(chapter)
- section = clean(section)
-
try:
context = {
'csrf': csrf(request)['csrf_token'],
@@ -202,8 +185,6 @@ def index(request, course_id, chapter=None, section=None,
look_for_module = chapter is not None and section is not None
if look_for_module:
- # TODO (cpennington): Pass the right course in here
-
section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user,
diff --git a/lms/templates/accordion.html b/lms/templates/accordion.html
index defb424a29..353b83db70 100644
--- a/lms/templates/accordion.html
+++ b/lms/templates/accordion.html
@@ -1,13 +1,13 @@
<%! from django.core.urlresolvers import reverse %>
<%def name="make_chapter(chapter)">
-