Commits

Anonymous committed fe0925e

[svn r101] added support for custom openid store.
Also start refactoring code.

works only with latest trunk.

Comments (0)

Files changed (9)

django_authopenid/__init__.py

 # -*- coding: utf-8 -*-
-# Copyright (c) 2007, 2008, Benoît Chesneau
+# Copyright 2007, 2008,2009 by Benoît Chesneau <benoitc@e-engura.org>
 # 
-# All rights reserved.
-# 
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-# 
-#      * Redistributions of source code must retain the above copyright
-#      * notice, this list of conditions and the following disclaimer.
-#      * Redistributions in binary form must reproduce the above copyright
-#      * notice, this list of conditions and the following disclaimer in the
-#      * documentation and/or other materials provided with the
-#      * distribution.  Neither the name of the <ORGANIZATION> nor the names
-#      * of its contributors may be used to endorse or promote products
-#      * derived from this software without specific prior written
-#      * permission.
-# 
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
-# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
-# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
-# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
 
 """
 Django authentification application to *with openid using django auth contrib/.
  * openid url
 """
 
-__version__ = "0.9.4"
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+try:
+    __version__ = __import__('pkg_resources').get_distribution('django_authopenid').version
+except:
+    __version__ = '?'
+
+# get openidstore to use.
+if not hasattr(settings, 'OPENID_STORE') or not settings.OPENID_STORE:
+    settings.OPENID_STORE = 'django_authopenid.openid_store.DjangoOpenIDStore'
+    
+def load_store(path):
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+    try:
+        mod = import_module(module)
+    except ImportError, e:
+        raise ImproperlyConfigured('Error importing openid store %s: "%s"' % (module, e))
+    except ValueError, e:
+        raise ImproperlyConfigured('Error importing openid store. Is OPENID_STORE a correctly defined list or tuple?')        
+    try:
+        cls = getattr(mod, attr)
+    except AttributeError:
+        raise ImproperlyConfigured, 'Module "%s" does not define a "%s" openid store' % (module, attr)
+    return cls
+    
+DjangoOpenIDStore = load_store(settings.OPENID_STORE)

django_authopenid/forms.py

     
 from django_authopenid.util import clean_next
 
-__all__ = ['OpenidSigninForm', 'OpenidAuthForm', 'OpenidVerifyForm',
-        'OpenidRegisterForm', 'RegistrationForm', 'ChangepwForm',
-        'ChangeemailForm', 'EmailPasswordForm', 'DeleteForm',
-        'ChangeOpenidForm', 'ChangeEmailForm', 'ChangepwForm']
-
 class OpenidSigninForm(forms.Form):
     """ signin form """
     openid_url = forms.CharField(max_length=255, 

django_authopenid/middleware.py

 # -*- coding: utf-8 -*-
-from django_authopenid import mimeparse
+# Copyright 2007, 2008,2009 by Benoît Chesneau <benoitc@e-engura.org>
+# 
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from django_authopenid.utils import mimeparse
 from django.http import HttpResponseRedirect
 from django.core.urlresolvers import reverse
 

django_authopenid/mimeparse.py

-"""MIME-Type Parser
-
-This module provides basic functions for handling mime-types. It can handle
-matching mime-types against a list of media-ranges. See section 14.1 of 
-the HTTP specification [RFC 2616] for a complete explaination.
-
-   http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
-
-Contents:
-    - parse_mime_type():   Parses a mime-type into it's component parts.
-    - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter.
-    - quality():           Determines the quality ('q') of a mime-type when compared against a list of media-ranges.
-    - quality_parsed():    Just like quality() except the second parameter must be pre-parsed.
-    - best_match():        Choose the mime-type with the highest quality ('q') from a list of candidates. 
-"""
-
-__version__ = "0.1.1"
-__author__ = 'Joe Gregorio'
-__email__ = "joe@bitworking.org"
-__credits__ = ""
-
-def parse_mime_type(mime_type):
-    """Carves up a mime_type and returns a tuple of the
-       (type, subtype, params) where 'params' is a dictionary
-       of all the parameters for the media range.
-       For example, the media range 'application/xhtml;q=0.5' would
-       get parsed into:
-
-       ('application', 'xhtml', {'q', '0.5'})
-       """
-    parts = mime_type.split(";")
-    params = dict([tuple([s.strip() for s in param.split("=")])\
-            for param in parts[1:] ])
-    (type, subtype) = parts[0].split("/")
-    return (type.strip(), subtype.strip(), params)
-
-def parse_media_range(range):
-    """Carves up a media range and returns a tuple of the
-       (type, subtype, params) where 'params' is a dictionary
-       of all the parameters for the media range.
-       For example, the media range 'application/*;q=0.5' would
-       get parsed into:
-
-       ('application', '*', {'q', '0.5'})
-
-       In addition this function also guarantees that there 
-       is a value for 'q' in the params dictionary, filling it
-       in with a proper default if necessary.
-       """
-    (type, subtype, params) = parse_mime_type(range)
-    if not params.has_key('q') or not params['q'] or \
-            not float(params['q']) or float(params['q']) > 1\
-            or float(params['q']) < 0:
-        params['q'] = '1'
-    return (type, subtype, params)
-
-def quality_parsed(mime_type, parsed_ranges):
-    """Find the best match for a given mime_type against 
-       a list of media_ranges that have already been 
-       parsed by parse_media_range(). Returns the 
-       'q' quality parameter of the best match, 0 if no
-       match was found. This function bahaves the same as quality()
-       except that 'parsed_ranges' must be a list of
-       parsed media ranges. """
-    best_fitness = -1 
-    best_match = ""
-    best_fit_q = 0
-    (target_type, target_subtype, target_params) =\
-            parse_media_range(mime_type)
-    for (type, subtype, params) in parsed_ranges:
-        param_matches = reduce(lambda x, y: x+y, [1 for (key, value) in \
-                target_params.iteritems() if key != 'q' and \
-                params.has_key(key) and value == params[key]], 0)
-        if (type == target_type or type == '*' or target_type == '*') and \
-                (subtype == target_subtype or subtype == '*' or target_subtype == '*'):
-            fitness = (type == target_type) and 100 or 0
-            fitness += (subtype == target_subtype) and 10 or 0
-            fitness += param_matches
-            if fitness > best_fitness:
-                best_fitness = fitness
-                best_fit_q = params['q']
-            
-    return float(best_fit_q)
-    
-def quality(mime_type, ranges):
-    """Returns the quality 'q' of a mime_type when compared
-    against the media-ranges in ranges. For example:
-
-    >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
-    0.7
-    
-    """ 
-    parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
-    return quality_parsed(mime_type, parsed_ranges)
-
-def best_match(supported, header):
-    """Takes a list of supported mime-types and finds the best
-    match for all the media-ranges listed in header. The value of
-    header must be a string that conforms to the format of the 
-    HTTP Accept: header. The value of 'supported' is a list of
-    mime-types.
-    
-    >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
-    'text/xml'
-    """
-    parsed_header = [parse_media_range(r) for r in header.split(",")]
-    weighted_matches = [(quality_parsed(mime_type, parsed_header), mime_type)\
-            for mime_type in supported]
-    weighted_matches.sort()
-    return weighted_matches[-1][0] and weighted_matches[-1][1] or ''
-
-if __name__ == "__main__":
-    import unittest
-
-    class TestMimeParsing(unittest.TestCase):
-
-        def test_parse_media_range(self):
-            self.assert_(('application', 'xml', {'q': '1'}) == parse_media_range('application/xml;q=1'))
-            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml'))
-            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml;q='))
-            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml ; q='))
-            self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=1;b=other'))
-            self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=2;b=other'))
-
-        def test_rfc_2616_example(self):
-            accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
-            self.assertEqual(1, quality("text/html;level=1", accept))
-            self.assertEqual(0.7, quality("text/html", accept))
-            self.assertEqual(0.3, quality("text/plain", accept))
-            self.assertEqual(0.5, quality("image/jpeg", accept))
-            self.assertEqual(0.4, quality("text/html;level=2", accept))
-            self.assertEqual(0.7, quality("text/html;level=3", accept))
-
-        def test_best_match(self):
-            mime_types_supported = ['application/xbel+xml', 'application/xml']
-            # direct match
-            self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml'), 'application/xbel+xml')
-            # direct match with a q parameter
-            self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml; q=1'), 'application/xbel+xml')
-            # direct match of our second choice with a q parameter
-            self.assertEqual(best_match(mime_types_supported, 'application/xml; q=1'), 'application/xml')
-            # match using a subtype wildcard
-            self.assertEqual(best_match(mime_types_supported, 'application/*; q=1'), 'application/xml')
-            # match using a type wildcard
-            self.assertEqual(best_match(mime_types_supported, '*/*'), 'application/xml')
-
-            mime_types_supported = ['application/xbel+xml', 'text/xml']
-            # match using a type versus a lower weighted subtype
-            self.assertEqual(best_match(mime_types_supported, 'text/*;q=0.5,*/*; q=0.1'), 'text/xml')
-            # fail to match anything
-            self.assertEqual(best_match(mime_types_supported, 'text/html,application/atom+xml; q=0.9'), '')
-
-        def test_support_wildcards(self):
-            mime_types_supported = ['image/*', 'application/xml']
-            # match using a type wildcard
-            self.assertEqual(best_match(mime_types_supported, 'image/png'), 'image/*')
-            # match using a wildcard for both requested and supported 
-            self.assertEqual(best_match(mime_types_supported, 'image/*'), 'image/*')
-
-    unittest.main() 

django_authopenid/models.py

 # -*- coding: utf-8 -*-
+# Copyright 2007, 2008,2009 by Benoît Chesneau <benoitc@e-engura.org>
+# 
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import random
+import sys
+import time
+try:
+    from hashlib import md5 as _md5
+except ImportError:
+    import md5
+    _md5 = md5.new
+
+
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.db import models
 
-import md5, random, sys, os, time
+
 
 __all__ = ['Nonce', 'Association', 'UserAssociation', 
         'UserPasswordQueueManager', 'UserPasswordQueue']

django_authopenid/util.py

-# -*- coding: utf-8 -*-
-from openid.store.interface import OpenIDStore
-from openid.association import Association as OIDAssociation
-from openid.extensions import sreg
-import openid.store
-
-from django.db.models.query import Q
-from django.conf import settings
-from django.http import str_to_unicode
-
-
-# needed for some linux distributions like debian
-try:
-    from openid.yadis import xri
-except:
-    from yadis import xri
-
-import time, base64, md5, operator
-import urllib
-
-from models import Association, Nonce
-
-__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next']
-
-DEFAULT_NEXT = getattr(settings, 'OPENID_REDIRECT_NEXT', '/')
-def clean_next(next):
-    if next is None:
-        return DEFAULT_NEXT
-    next = str_to_unicode(urllib.unquote(next), 'utf-8')
-    next = next.strip()
-    if next.startswith('/'):
-        return next
-    return DEFAULT_NEXT
-
-class OpenID:
-    def __init__(self, openid_, issued, attrs=None, sreg_=None):
-        self.openid = openid_
-        self.issued = issued
-        self.attrs = attrs or {}
-        self.sreg = sreg_ or {}
-        self.is_iname = (xri.identifierScheme(openid_) == 'XRI')
-    
-    def __repr__(self):
-        return '<OpenID: %s>' % self.openid
-    
-    def __str__(self):
-        return self.openid
-
-class DjangoOpenIDStore(OpenIDStore):
-    def __init__(self):
-        self.max_nonce_age = 6 * 60 * 60 # Six hours
-    
-    def storeAssociation(self, server_url, association):
-        assoc = Association(
-            server_url = server_url,
-            handle = association.handle,
-            secret = base64.encodestring(association.secret),
-            issued = association.issued,
-            lifetime = association.issued,
-            assoc_type = association.assoc_type
-        )
-        assoc.save()
-    
-    def getAssociation(self, server_url, handle=None):
-        assocs = []
-        if handle is not None:
-            assocs = Association.objects.filter(
-                server_url = server_url, handle = handle
-            )
-        else:
-            assocs = Association.objects.filter(
-                server_url = server_url
-            )
-        if not assocs:
-            return None
-        associations = []
-        for assoc in assocs:
-            association = OIDAssociation(
-                assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
-                assoc.lifetime, assoc.assoc_type
-            )
-            if association.getExpiresIn() == 0:
-                self.removeAssociation(server_url, assoc.handle)
-            else:
-                associations.append((association.issued, association))
-        if not associations:
-            return None
-        return associations[-1][1]
-    
-    def removeAssociation(self, server_url, handle):
-        assocs = list(Association.objects.filter(
-            server_url = server_url, handle = handle
-        ))
-        assocs_exist = len(assocs) > 0
-        for assoc in assocs:
-            assoc.delete()
-        return assocs_exist
-
-    def useNonce(self, server_url, timestamp, salt):
-        if abs(timestamp - time.time()) > openid.store.nonce.SKEW:
-            return False
-        
-        query = [
-                Q(server_url__exact=server_url),
-                Q(timestamp__exact=timestamp),
-                Q(salt__exact=salt),
-        ]
-        try:
-            ononce = Nonce.objects.get(reduce(operator.and_, query))
-        except Nonce.DoesNotExist:
-            ononce = Nonce(
-                    server_url=server_url,
-                    timestamp=timestamp,
-                    salt=salt
-            )
-            ononce.save()
-            return True
-        
-        ononce.delete()
-
-        return False
-   
-    def cleanupNonce(self):
-        Nonce.objects.filter(timestamp<int(time.time()) - nonce.SKEW).delete()
-
-    def cleanupAssociations(self):
-        Association.objects.extra(where=['issued + lifetimeint<(%s)' % time.time()]).delete()
-
-    def getAuthKey(self):
-        # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY
-        return md5.new(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]
-    
-    def isDumb(self):
-        return False
-
-def from_openid_response(openid_response):
-    """ return openid object from response """
-    issued = int(time.time())
-    sreg_resp = sreg.SRegResponse.fromSuccessResponse(openid_response) \
-            or []
-    
-    return OpenID(
-        openid_response.identity_url, issued, openid_response.signed_fields, 
-         dict(sreg_resp)
-    )

django_authopenid/utils/__init__.py

+# -*- coding: utf-8 -*-
+
+import time
+import urllib
+
+from django.conf import settings
+from django.http import str_to_unicode
+from openid.extensions import sreg
+
+try: # needed for some linux distributions like debian
+    from openid.yadis import xri
+except ImportError:
+    from yadis import xri
+
+__all__ = ['OpenID', 'from_openid_response', 'clean_next']
+
+class OpenID(object):
+    def __init__(self, openid_, issued, attrs=None, sreg_=None):
+        self.openid = openid_
+        self.issued = issued
+        self.attrs = attrs or {}
+        self.sreg = sreg_ or {}
+        self.is_iname = (xri.identifierScheme(openid_) == 'XRI')
+    
+    def __repr__(self):
+        return '<OpenID: %s>' % self.openid
+    
+    def __str__(self):
+        return self.openid
+
+DEFAULT_NEXT = getattr(settings, 'OPENID_REDIRECT_NEXT', '/')
+def clean_next(next):
+    if next is None:
+        return DEFAULT_NEXT
+    next = str_to_unicode(urllib.unquote(next), 'utf-8')
+    next = next.strip()
+    if next.startswith('/'):
+        return next
+    return DEFAULT_NEXT
+
+
+def from_openid_response(openid_response):
+    """ return openid object from response """
+    issued = int(time.time())
+    sreg_resp = sreg.SRegResponse.fromSuccessResponse(openid_response) \
+            or []
+    
+    return OpenID(
+        openid_response.identity_url, issued, openid_response.signed_fields, 
+         dict(sreg_resp)
+    )

django_authopenid/utils/mimeparse.py

+"""MIME-Type Parser
+
+This module provides basic functions for handling mime-types. It can handle
+matching mime-types against a list of media-ranges. See section 14.1 of 
+the HTTP specification [RFC 2616] for a complete explaination.
+
+   http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+Contents:
+    - parse_mime_type():   Parses a mime-type into it's component parts.
+    - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter.
+    - quality():           Determines the quality ('q') of a mime-type when compared against a list of media-ranges.
+    - quality_parsed():    Just like quality() except the second parameter must be pre-parsed.
+    - best_match():        Choose the mime-type with the highest quality ('q') from a list of candidates. 
+"""
+
+__version__ = "0.1.1"
+__author__ = 'Joe Gregorio'
+__email__ = "joe@bitworking.org"
+__credits__ = ""
+
+def parse_mime_type(mime_type):
+    """Carves up a mime_type and returns a tuple of the
+       (type, subtype, params) where 'params' is a dictionary
+       of all the parameters for the media range.
+       For example, the media range 'application/xhtml;q=0.5' would
+       get parsed into:
+
+       ('application', 'xhtml', {'q', '0.5'})
+       """
+    parts = mime_type.split(";")
+    params = dict([tuple([s.strip() for s in param.split("=")])\
+            for param in parts[1:] ])
+    (type, subtype) = parts[0].split("/")
+    return (type.strip(), subtype.strip(), params)
+
+def parse_media_range(range):
+    """Carves up a media range and returns a tuple of the
+       (type, subtype, params) where 'params' is a dictionary
+       of all the parameters for the media range.
+       For example, the media range 'application/*;q=0.5' would
+       get parsed into:
+
+       ('application', '*', {'q', '0.5'})
+
+       In addition this function also guarantees that there 
+       is a value for 'q' in the params dictionary, filling it
+       in with a proper default if necessary.
+       """
+    (type, subtype, params) = parse_mime_type(range)
+    if not params.has_key('q') or not params['q'] or \
+            not float(params['q']) or float(params['q']) > 1\
+            or float(params['q']) < 0:
+        params['q'] = '1'
+    return (type, subtype, params)
+
+def quality_parsed(mime_type, parsed_ranges):
+    """Find the best match for a given mime_type against 
+       a list of media_ranges that have already been 
+       parsed by parse_media_range(). Returns the 
+       'q' quality parameter of the best match, 0 if no
+       match was found. This function bahaves the same as quality()
+       except that 'parsed_ranges' must be a list of
+       parsed media ranges. """
+    best_fitness = -1 
+    best_match = ""
+    best_fit_q = 0
+    (target_type, target_subtype, target_params) =\
+            parse_media_range(mime_type)
+    for (type, subtype, params) in parsed_ranges:
+        param_matches = reduce(lambda x, y: x+y, [1 for (key, value) in \
+                target_params.iteritems() if key != 'q' and \
+                params.has_key(key) and value == params[key]], 0)
+        if (type == target_type or type == '*' or target_type == '*') and \
+                (subtype == target_subtype or subtype == '*' or target_subtype == '*'):
+            fitness = (type == target_type) and 100 or 0
+            fitness += (subtype == target_subtype) and 10 or 0
+            fitness += param_matches
+            if fitness > best_fitness:
+                best_fitness = fitness
+                best_fit_q = params['q']
+            
+    return float(best_fit_q)
+    
+def quality(mime_type, ranges):
+    """Returns the quality 'q' of a mime_type when compared
+    against the media-ranges in ranges. For example:
+
+    >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
+    0.7
+    
+    """ 
+    parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
+    return quality_parsed(mime_type, parsed_ranges)
+
+def best_match(supported, header):
+    """Takes a list of supported mime-types and finds the best
+    match for all the media-ranges listed in header. The value of
+    header must be a string that conforms to the format of the 
+    HTTP Accept: header. The value of 'supported' is a list of
+    mime-types.
+    
+    >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
+    'text/xml'
+    """
+    parsed_header = [parse_media_range(r) for r in header.split(",")]
+    weighted_matches = [(quality_parsed(mime_type, parsed_header), mime_type)\
+            for mime_type in supported]
+    weighted_matches.sort()
+    return weighted_matches[-1][0] and weighted_matches[-1][1] or ''
+
+if __name__ == "__main__":
+    import unittest
+
+    class TestMimeParsing(unittest.TestCase):
+
+        def test_parse_media_range(self):
+            self.assert_(('application', 'xml', {'q': '1'}) == parse_media_range('application/xml;q=1'))
+            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml'))
+            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml;q='))
+            self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml ; q='))
+            self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=1;b=other'))
+            self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=2;b=other'))
+
+        def test_rfc_2616_example(self):
+            accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
+            self.assertEqual(1, quality("text/html;level=1", accept))
+            self.assertEqual(0.7, quality("text/html", accept))
+            self.assertEqual(0.3, quality("text/plain", accept))
+            self.assertEqual(0.5, quality("image/jpeg", accept))
+            self.assertEqual(0.4, quality("text/html;level=2", accept))
+            self.assertEqual(0.7, quality("text/html;level=3", accept))
+
+        def test_best_match(self):
+            mime_types_supported = ['application/xbel+xml', 'application/xml']
+            # direct match
+            self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml'), 'application/xbel+xml')
+            # direct match with a q parameter
+            self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml; q=1'), 'application/xbel+xml')
+            # direct match of our second choice with a q parameter
+            self.assertEqual(best_match(mime_types_supported, 'application/xml; q=1'), 'application/xml')
+            # match using a subtype wildcard
+            self.assertEqual(best_match(mime_types_supported, 'application/*; q=1'), 'application/xml')
+            # match using a type wildcard
+            self.assertEqual(best_match(mime_types_supported, '*/*'), 'application/xml')
+
+            mime_types_supported = ['application/xbel+xml', 'text/xml']
+            # match using a type versus a lower weighted subtype
+            self.assertEqual(best_match(mime_types_supported, 'text/*;q=0.5,*/*; q=0.1'), 'text/xml')
+            # fail to match anything
+            self.assertEqual(best_match(mime_types_supported, 'text/html,application/atom+xml; q=0.9'), '')
+
+        def test_support_wildcards(self):
+            mime_types_supported = ['image/*', 'application/xml']
+            # match using a type wildcard
+            self.assertEqual(best_match(mime_types_supported, 'image/png'), 'image/*')
+            # match using a wildcard for both requested and supported 
+            self.assertEqual(best_match(mime_types_supported, 'image/*'), 'image/*')
+
+    unittest.main() 

django_authopenid/views.py

 import re
 import urllib
 
+from django_authopenid import DjangoOpenIDStore
+from django_authopenid.forms import *
+from django_authopenid.models import UserAssociation, UserPasswordQueue
+from django_authopenid.utils import *
 
-from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, clean_next
-from django_authopenid.models import UserAssociation, UserPasswordQueue
-from django_authopenid.forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \
-        OpenidVerifyForm, RegistrationForm, ChangepwForm, ChangeemailForm, \
-        ChangeopenidForm, DeleteForm, EmailPasswordForm
 
 def get_url_host(request):
     if request.is_secure():
     return get_url_host(request) + request.get_full_path()
 
 
-
 def ask_openid(request, openid_url, redirect_to, on_failure=None,
         sreg_request=None):
     """ basic function to ask openid and return response """
     redirect = "%s?msg=%s" % (reverse('user_signin'), 
                                         urlquote_plus(msg))
 
-    return HttpResponseRedirect(redirect)
+    return HttpResponseRedirect(redirect)