├── .gitignore ├── LICENSE ├── README.rst ├── browsecap ├── __init__.py ├── browscap.ini ├── browser.py └── middleware.py ├── debian ├── README.Debian ├── changelog ├── compat ├── control ├── copyright ├── pycompat ├── pyversions └── rules ├── setup.cfg ├── setup.py └── tests └── unit_project ├── __init__.py ├── manage.py ├── run_tests.py ├── settings ├── __init__.py ├── base.py ├── config.py └── local_example.py ├── test_browser_detection.py └── test_middleware.py /.gitignore: -------------------------------------------------------------------------------- 1 | /browsecap.egg-info/ 2 | 3 | *.py[co] 4 | .*.sw? 5 | *~ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Centrum Holdings 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the Centrum Holdings nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django browsecap 2 | ================ 3 | 4 | Browsecap is a simple library for django for detecting browser type. 5 | 6 | The main interface consists of two function in `browsecap.browser`: 7 | 8 | `is_mobile`: returns True if the given user agent is a known mobile browser 9 | 10 | `is_crawler`: returns True if the given user agent is a known crawler 11 | 12 | MobileRedirectMiddleware 13 | ------------------------ 14 | 15 | For your convenience there is also a middleware that automatically redirects 16 | all mobile users to alternate domain. 17 | 18 | To use just add `browsecap.middleware.MobileRedirectMiddleware` to your 19 | `settings.MIDDLEWARE_CLASSES` and define a `MOBILE_DOMAIN` that you want your 20 | mobile users redirected to. Note that the value must contain full path 21 | including the protocol (http://) 22 | 23 | The middleware sets ismobile cookie to value 1 and can be overriden by deleting 24 | that cookie setting isbrowser cookie to 1. 25 | 26 | Internals 27 | --------- 28 | 29 | Browsecap works by parsing the browscap.ini file and storing a list of browsers 30 | as regexps in memory. Each user agent to be checked is then matched against the 31 | set of regexps until we run out (False) or a match is found (True). The result 32 | is stored in a dictionary to speedup further processing of the same user agent 33 | (in our experience, 200k users only have around 8k distinct user agents, so 34 | caching works). 35 | 36 | Performance of the matchig is adequate and shouldn't slow down the request 37 | processing even if used every time (middleware), the only thing that is 38 | somewhat slow (under a second on a laptop) is parsing the browscap.ini file. 39 | This is done only when the module is first loaded and stores it's results in 40 | cache so that start of the next thread/process should not be hindered. 41 | 42 | You can provide your own browscap.ini file by setting `BROWSCAP_DIR` in django 43 | settings pointing to a directory containing the file. 44 | 45 | Credits 46 | ------- 47 | It is based on henning's snippet #267 - 48 | http://www.djangosnippets.org/snippets/267/ 49 | 50 | Thank you very much for the great work! 51 | -------------------------------------------------------------------------------- /browsecap/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION = (0, 1, 0, 0) 3 | 4 | __version__ = VERSION 5 | __versionstr__ = '.'.join(map(str, VERSION)) 6 | 7 | -------------------------------------------------------------------------------- /browsecap/browser.py: -------------------------------------------------------------------------------- 1 | from ConfigParser import SafeConfigParser as ConfigParser 2 | import re 3 | import os 4 | 5 | from django.core.cache import cache 6 | from django.conf import settings 7 | 8 | CACHE_KEY = 'browsecap' 9 | CACHE_TIMEOUT = 60*60*2 # 2 hours 10 | DEFAULT_BC_PATH = os.path.abspath(os.path.dirname(__file__ or os.getcwd())) 11 | 12 | class MobileBrowserParser(object): 13 | def __new__(cls, *args, **kwargs): 14 | # Only create one instance of this clas 15 | if "instance" not in cls.__dict__: 16 | cls.instance = object.__new__(cls, *args, **kwargs) 17 | return cls.instance 18 | 19 | def __init__(self): 20 | self.mobile_cache = {} 21 | self.crawler_cache = {} 22 | self.parse() 23 | 24 | def parse(self): 25 | # try egtting the parsed definitions from cache 26 | data = cache.get(CACHE_KEY) 27 | if data: 28 | self.mobile_browsers = map(re.compile, data['mobile_browsers']) 29 | self.crawlers = map(re.compile, data['crawlers']) 30 | return 31 | 32 | # parse browscap.ini 33 | cfg = ConfigParser() 34 | files = ("browscap.ini", "bupdate.ini") 35 | base_path = getattr(settings, 'BROWSCAP_DIR', DEFAULT_BC_PATH) 36 | read_ok = cfg.read([os.path.join(base_path, name) for name in files]) 37 | if len(read_ok) == 0: 38 | raise IOError, "Could not read browscap.ini, " + \ 39 | "please get it from http://www.GaryKeith.com" 40 | 41 | browsers = {} 42 | parents = set() 43 | 44 | # go through all the browsers and record their parents 45 | for name in cfg.sections(): 46 | sec = dict(cfg.items(name)) 47 | p = sec.get("parent") 48 | if p: 49 | parents.add(p) 50 | browsers[name] = sec 51 | 52 | self.mobile_browsers = [] 53 | self.crawlers = [] 54 | for name, conf in browsers.items(): 55 | # only process those that are not abstract parents 56 | if name in parents: 57 | continue 58 | 59 | p = conf.get('parent') 60 | if p: 61 | # update config based on parent's settings 62 | parent = browsers[p] 63 | conf.update(parent) 64 | 65 | # we only care for mobiles and crawlers 66 | if conf.get('ismobiledevice', 'false') == 'true' or conf.get('crawler', 'false') == 'true': 67 | qname = re.escape(name) 68 | qname = qname.replace("\\?", ".").replace("\\*", ".*?") 69 | qname = "^%s$" % qname 70 | 71 | # register the user agent 72 | if conf.get('ismobiledevice', 'false') == 'true': 73 | self.mobile_browsers.append(qname) 74 | 75 | if conf.get('crawler', 'false') == 'true': 76 | self.crawlers.append(qname) 77 | 78 | # store in cache to speed up next load 79 | cache.set(CACHE_KEY, {'mobile_browsers': self.mobile_browsers, 'crawlers': self.crawlers}, CACHE_TIMEOUT) 80 | 81 | # compile regexps 82 | self.mobile_browsers = map(re.compile, self.mobile_browsers) 83 | self.crawlers = map(re.compile, self.crawlers) 84 | 85 | def find_in_list(self, useragent, agent_list, cache): 86 | 'Check useragent against agent_list of regexps.' 87 | try: 88 | return cache[useragent] 89 | except KeyError, e: 90 | pass 91 | 92 | for sec_pat in agent_list: 93 | if sec_pat.match(useragent): 94 | out = True 95 | break 96 | else: 97 | out = False 98 | cache[useragent] = out 99 | return out 100 | 101 | def is_mobile(self, useragent): 102 | 'Returns True if the given useragent is a known mobile browser, False otherwise.' 103 | return self.find_in_list(useragent, self.mobile_browsers, self.mobile_cache) 104 | 105 | def is_crawler(self, useragent): 106 | 'Returns True if the given useragent is a known crawler, False otherwise.' 107 | return self.find_in_list(useragent, self.crawlers, self.crawler_cache) 108 | 109 | 110 | # instantiate the parser 111 | browsers = MobileBrowserParser() 112 | 113 | # provide access to methods as functions for convenience 114 | is_mobile = browsers.is_mobile 115 | is_crawler = browsers.is_crawler 116 | 117 | 118 | def update(): 119 | 'Download new version of browsecap.ini' 120 | import urllib 121 | urllib.urlretrieve("http://browsers.garykeith.com/stream.asp?BrowsCapINI", 122 | "browscap.ini") 123 | 124 | 125 | -------------------------------------------------------------------------------- /browsecap/middleware.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from django.http import HttpResponseRedirect 4 | from django.utils.http import cookie_date 5 | from django.conf import settings 6 | 7 | from browsecap.browser import is_mobile 8 | 9 | # default cookie expire time is one month 10 | DEFAULT_COOKIE_MAX_AGE = 3600*24*31 11 | 12 | class MobileRedirectMiddleware(object): 13 | def process_request(self, request): 14 | if not getattr(settings, 'MOBILE_DOMAIN', False): 15 | return 16 | 17 | # test for mobile browser 18 | if ( 19 | # check for override cookie, do not check if present 20 | request.COOKIES.get('ismobile', '0') == '1' or ( 21 | # browser info present 22 | 'HTTP_USER_AGENT' in request.META 23 | and 24 | # desktop browser override not set 25 | request.COOKIES.get('isbrowser', '0') != '1' 26 | and 27 | # check browser type 28 | is_mobile(request.META['HTTP_USER_AGENT']) 29 | ) 30 | ): 31 | redirect = settings.MOBILE_DOMAIN 32 | if getattr(settings, 'MOBILE_REDIRECT_PRESERVE_URL', False): 33 | redirect = redirect.rstrip('/') + request.path_info 34 | # redirect to mobile domain 35 | response = HttpResponseRedirect(redirect) 36 | 37 | # set cookie to identify the browser as mobile 38 | max_age = getattr(settings, 'MOBILE_COOKIE_MAX_AGE', DEFAULT_COOKIE_MAX_AGE) 39 | expires_time = time.time() + max_age 40 | expires = cookie_date(expires_time) 41 | response.set_cookie('ismobile', '1', domain=settings.SESSION_COOKIE_DOMAIN, max_age=max_age, expires=expires) 42 | return response 43 | 44 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | centrum-python-browsecap for debian 2 | ------------------------------------------- 3 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | centrum-python-browsecap (0.0.0) unstable; urgency=low 2 | 3 | * initial build 4 | 5 | -- Jakub Vysoky Fri, 13 Mar 2009 16:37:07 +0100 6 | 7 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: centrum-python-browsecap 2 | Section: python 3 | Priority: optional 4 | Maintainer: Jan Kral 5 | Uploaders: Ondrej Kohout , Jakub Vysoky , Lukas Linhart 6 | Build-Depends: cdbs (>= 0.4.41), debhelper (>= 5.0.37.2), python-dev, python-support (>= 0.3), python-setuptools, centrum-python-setuptoolsdummy 7 | Standards-Version: 3.7.2 8 | 9 | Package: centrum-python-browsecap 10 | Architecture: all 11 | Depends: ${python:Depends}, ${misc:Depends}, python-django, python-docutils 12 | Description: django base library 13 | 14 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | bsd 2 | 3 | -------------------------------------------------------------------------------- /debian/pycompat: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /debian/pyversions: -------------------------------------------------------------------------------- 1 | 2.3- 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- mode: makefile; coding: utf-8 -*- 3 | 4 | DEB_PYTHON_SYSTEM=pysupport 5 | include /usr/share/cdbs/1/rules/debhelper.mk 6 | include /usr/share/cdbs/1/class/python-distutils.mk 7 | 8 | DEB_AUTO_CLEANUP_RCS := yes 9 | #DEB_INSTALL_CHANGELOGS_ALL := CHANGE_LOG.txt 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = dev 3 | ;tag_svn_revision = true 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import browsecap 3 | 4 | VERSION = (0, 1, 0, 0) 5 | __version__ = VERSION 6 | __versionstr__ = '.'.join(map(str, VERSION)) 7 | 8 | setup( 9 | name = 'browsecap', 10 | version = __versionstr__, 11 | description = 'Django Base Library', 12 | long_description = '\n'.join(( 13 | 'Django browsecap', 14 | '', 15 | 'helper module to process browseap.ini', 16 | 'designed mainly to detect mobile browsers and crawlers.', 17 | '', 18 | 'Based Heavily on django snippet 267', 19 | 'Thanks!', 20 | )), 21 | author = 'centrum holdings s.r.o', 22 | author_email='devel@centrumholdings.com', 23 | license = 'BSD', 24 | url='http://www.github.com/ella/django-browsecap', 25 | 26 | packages = find_packages( 27 | where = '.', 28 | exclude = ('docs', 'tests') 29 | ), 30 | 31 | include_package_data = True, 32 | 33 | classifiers=[ 34 | "Development Status :: 4 - Beta", 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: BSD License", 37 | "Operating System :: OS Independent", 38 | "Framework :: Django", 39 | "Programming Language :: Python :: 2.5", 40 | "Programming Language :: Python :: 2.6", 41 | "Topic :: Software Development :: Libraries :: Python Modules", 42 | ], 43 | install_requires = [ 44 | 'setuptools>=0.6b1', 45 | ], 46 | setup_requires = [ 47 | 'setuptools_dummy', 48 | ], 49 | ) 50 | 51 | -------------------------------------------------------------------------------- /tests/unit_project/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this package, You can find test environment for Ella unittest project. 3 | As only true unittest and "unittest" (test testing programming unit, but using 4 | database et al) are there, there is not much setup around. 5 | 6 | If You're looking for example project, take a look into example_project directory. 7 | """ -------------------------------------------------------------------------------- /tests/unit_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from os.path import join, pardir, abspath, dirname, split 5 | import sys 6 | 7 | from django.core.management import execute_from_command_line 8 | 9 | 10 | # fix PYTHONPATH and DJANGO_SETTINGS for us 11 | # django settings module 12 | DJANGO_SETTINGS_MODULE = '%s.%s' % (split(abspath(dirname(__file__)))[1], 'settings') 13 | # pythonpath dirs 14 | PYTHONPATH = [ 15 | abspath(join( dirname(__file__), pardir, pardir)), 16 | abspath(join( dirname(__file__), pardir)), 17 | ] 18 | 19 | # inject few paths to pythonpath 20 | for p in PYTHONPATH: 21 | if p not in sys.path: 22 | sys.path.insert(0, p) 23 | 24 | # django needs this env variable 25 | os.environ['DJANGO_SETTINGS_MODULE'] = DJANGO_SETTINGS_MODULE 26 | 27 | 28 | if __name__ == "__main__": 29 | execute_from_command_line() 30 | 31 | -------------------------------------------------------------------------------- /tests/unit_project/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | simple shortcut for running nosetests via python 5 | replacement for *.bat or *.sh wrappers 6 | ''' 7 | 8 | import os 9 | import sys 10 | from os.path import join, pardir, abspath, dirname, split 11 | 12 | import nose 13 | 14 | 15 | # django settings module 16 | DJANGO_SETTINGS_MODULE = '%s.%s' % (split(abspath(dirname(__file__)))[1], 'settings') 17 | # pythonpath dirs 18 | PYTHONPATH = [ 19 | abspath(join( dirname(__file__), pardir, pardir)), 20 | abspath(join( dirname(__file__), pardir)), 21 | ] 22 | 23 | 24 | # inject few paths to pythonpath 25 | for p in PYTHONPATH: 26 | if p not in sys.path: 27 | sys.path.insert(0, p) 28 | 29 | # django needs this env variable 30 | os.environ['DJANGO_SETTINGS_MODULE'] = DJANGO_SETTINGS_MODULE 31 | 32 | 33 | # TODO: ugly hack to inject django plugin to nose.run 34 | # 35 | # 36 | for i in ['--with-django',]: 37 | if i not in sys.argv: 38 | sys.argv.insert(1, i) 39 | 40 | 41 | nose.run_exit( 42 | defaultTest=dirname(__file__), 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /tests/unit_project/settings/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings package is acting exactly like settings module in standard django projects. 3 | However, settings combines two distinct things: 4 | (1) General project configuration, which is property of the project 5 | (like which application to use, URL configuration, authentication backends...) 6 | (2) Machine-specific environment configuration (database to use, cache URL, ...) 7 | 8 | Thus, we're changing module into package: 9 | * base.py contains (1), so no adjustments there should be needed to make project 10 | on your machine 11 | * config.py contains (2) with sensible default values that should make project 12 | runnable on most expected machines 13 | * local.py contains (2) for your specific machine. File your defaults there. 14 | """ 15 | 16 | from unit_project.settings.base import * 17 | from unit_project.settings.config import * 18 | 19 | try: 20 | from unit_project.settings.local import * 21 | except ImportError: 22 | pass 23 | -------------------------------------------------------------------------------- /tests/unit_project/settings/base.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, join, normpath, pardir 2 | 3 | FILE_ROOT = normpath(join(dirname(__file__), pardir)) 4 | 5 | USE_I18N = True 6 | 7 | MEDIA_ROOT = join(FILE_ROOT, 'static') 8 | 9 | MEDIA_URL = '/static' 10 | 11 | ADMIN_MEDIA_PREFIX = '/admin_media/' 12 | 13 | 14 | # List of callables that know how to import templates from various sources. 15 | TEMPLATE_LOADERS = ( 16 | 'unit_project.template_loader.load_template_source', 17 | ) 18 | 19 | MIDDLEWARE_CLASSES = ( 20 | 'django.middleware.common.CommonMiddleware', 21 | 'django.contrib.sessions.middleware.SessionMiddleware', 22 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 23 | ) 24 | 25 | ROOT_URLCONF = 'browsecap.sample.urls' 26 | 27 | TEMPLATE_DIRS = ( 28 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 29 | # Always use forward slashes, even on Windows. 30 | # Don't forget to use absolute paths, not relative paths. 31 | join(FILE_ROOT, 'templates'), 32 | 33 | ) 34 | 35 | TEMPLATE_CONTEXT_PROCESSORS = ( 36 | 'django.core.context_processors.media', 37 | ) 38 | 39 | INSTALLED_APPS = ( 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.sites', 44 | 'django.contrib.redirects', 45 | 'django.contrib.admin', 46 | ) 47 | 48 | DEFAULT_PAGE_ID = 1 49 | 50 | VERSION = 1 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/unit_project/settings/config.py: -------------------------------------------------------------------------------- 1 | from tempfile import gettempdir 2 | from os.path import join 3 | 4 | ADMINS = ( 5 | # ('Your Name', 'your_email@domain.com'), 6 | ) 7 | 8 | MANAGERS = ADMINS 9 | 10 | 11 | DEBUG = True 12 | TEMPLATE_DEBUG = DEBUG 13 | DISABLE_CACHE_TEMPLATE = DEBUG 14 | 15 | 16 | DATABASE_ENGINE = 'sqlite3' 17 | DATABASE_NAME = join(gettempdir(), 'browsecap_unit_project.db') 18 | TEST_DATABASE_NAME =join(gettempdir(), 'test_unit_project.db') 19 | DATABASE_USER = '' 20 | DATABASE_PASSWORD = '' 21 | DATABASE_HOST = '' 22 | DATABASE_PORT = '' 23 | 24 | 25 | TIME_ZONE = 'Europe/Prague' 26 | 27 | LANGUAGE_CODE = 'en-us' 28 | 29 | SITE_ID = 1 30 | 31 | # Make this unique, and don't share it with anybody. 32 | SECRET_KEY = '88b-01f^x4lh$-s5-hdccnicekg07)niir2g6)93!0#k(=mfv$' 33 | 34 | # TODO: Fix logging 35 | # init logger 36 | #LOGGING_CONFIG_FILE = join(dirname(testbed.__file__), 'settings', 'logger.ini') 37 | #if isinstance(LOGGING_CONFIG_FILE, basestring) and isfile(LOGGING_CONFIG_FILE): 38 | # logging.config.fileConfig(LOGGING_CONFIG_FILE) 39 | 40 | # we want to reset whole cache in test 41 | # until we do that, don't use cache 42 | CACHE_BACKEND = 'dummy://' 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/unit_project/settings/local_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rename to local.py and set variables from config.py that 3 | You want to override. 4 | """ 5 | -------------------------------------------------------------------------------- /tests/unit_project/test_browser_detection.py: -------------------------------------------------------------------------------- 1 | from djangosanetesting import UnitTestCase 2 | 3 | from browsecap.browser import is_mobile, is_crawler 4 | 5 | class TestIsMobileDetection(UnitTestCase): 6 | mobile = [ 7 | 'Opera/9.60 (J2ME/MIDP; Opera Mini/4.2.13337/504; U; cs) Presto/2.2.0', 8 | 'BlackBerry9000/4.6.0.126 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/170', 9 | 'Mozilla/5.0 (PLAYSTATION 3; 1.00)', 10 | 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; cs-cz) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16', 11 | 'Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/31.0.017; Profile/MIDP-2.0 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413', 12 | 'Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413', 13 | ] 14 | desktop = [ 15 | 'Windows-RSS-Platform/2.0 (MSIE 8.0; Windows NT 5.1)', 16 | 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB6; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 17 | 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 18 | 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; GTB6; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 19 | 'Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)', 20 | 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081202 Iceweasel/2.0.0.19 (Debian-2.0.0.19-0etch1)', 21 | 'Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.0.11) Gecko/2009060215 (CK-Stahuj.cz) Firefox/3.0.11 (.NET CLR 2.0.50727)', 22 | 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; cs-cz) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/(null) Safari/525.27.1', 23 | 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; cs; rv:1.9.0.11) Gecko/2009060214 Firefox/3.0.11', 24 | 'Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.4.4', 25 | 'Opera/9.64 (Windows NT 5.1; U; cs) Presto/2.1.1', 26 | 'Opera/9.52 (X11; Linux i686; U; en)', 27 | 'Wget/1.10.2', 28 | 29 | ] 30 | def test_returns_false_for_empty_user_agent(self): 31 | self.assert_false(is_mobile('')) 32 | 33 | def test_returns_false_for_unknown_browser(self): 34 | self.assert_false(is_mobile('Unknown')) 35 | 36 | def test_identify_known_desktop_browsers(self): 37 | fails = [] 38 | for m in self.desktop: 39 | if is_mobile(m): 40 | fails.append(m) 41 | self.assert_equals([], fails) 42 | 43 | def test_identify_known_mobile_browsers(self): 44 | fails = [] 45 | for m in self.mobile: 46 | if not is_mobile(m): 47 | fails.append(m) 48 | self.assert_equals([], fails) 49 | 50 | class TestIsCrawlerDetection(UnitTestCase): 51 | crawler = [ 52 | 'Googlebot-Image/1.0 ( http://www.googlebot.com/bot.html)', 53 | 'Mozilla/5.0 (compatible; Yahoo! Slurp/3.0; http://help.yahoo.com/help/us/ysearch/slurp)', 54 | 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', 55 | 'SeznamBot/2.0 (+http://fulltext.sblog.cz/robot/)', 56 | 'SeznamBot/1.0 (+http://fulltext.seznam.cz/) ', 57 | 'msnbot/1.1 (+http://search.msn.com/msnbot.htm)', 58 | 'Baiduspider+(+http://www.baidu.com/search/spider_jp.html) ', 59 | ] 60 | 61 | desktop = [ 62 | 'Windows-RSS-Platform/2.0 (MSIE 8.0; Windows NT 5.1)', 63 | 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB6; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 64 | 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)', 65 | 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; GTB6; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 66 | 'Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)', 67 | 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081202 Iceweasel/2.0.0.19 (Debian-2.0.0.19-0etch1)', 68 | 'Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.0.11) Gecko/2009060215 (CK-Stahuj.cz) Firefox/3.0.11 (.NET CLR 2.0.50727)', 69 | 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_4_11; cs-cz) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/(null) Safari/525.27.1', 70 | 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; cs; rv:1.9.0.11) Gecko/2009060214 Firefox/3.0.11', 71 | 'Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.4.4', 72 | 'Opera/9.64 (Windows NT 5.1; U; cs) Presto/2.1.1', 73 | 'Opera/9.52 (X11; Linux i686; U; en)', 74 | ] 75 | def test_returns_false_for_empty_user_agent(self): 76 | self.assert_false(is_crawler('')) 77 | 78 | def test_returns_false_for_unknown_browser(self): 79 | self.assert_false(is_crawler('Unknown')) 80 | 81 | def test_identify_known_desktop_browsers(self): 82 | fails = [] 83 | for m in self.desktop: 84 | if is_crawler(m): 85 | fails.append(m) 86 | self.assert_equals([], fails) 87 | 88 | def test_identify_known_crawler_browsers(self): 89 | fails = [] 90 | for m in self.crawler: 91 | if not is_crawler(m): 92 | fails.append(m) 93 | self.assert_equals([], fails) -------------------------------------------------------------------------------- /tests/unit_project/test_middleware.py: -------------------------------------------------------------------------------- 1 | from djangosanetesting import UnitTestCase 2 | 3 | from django.http import HttpRequest, HttpResponseRedirect 4 | from django.conf import settings 5 | 6 | from browsecap.middleware import MobileRedirectMiddleware 7 | 8 | def build_request(user_agent='', cookies={}): 9 | """ 10 | Returns request object with useful attributes 11 | """ 12 | request = HttpRequest() 13 | # Session and cookies 14 | request.session = {} 15 | request.COOKIES = cookies 16 | request.META['HTTP_USER_AGENT'] = user_agent 17 | return request 18 | 19 | class TestMobileRedirectMiddleware(UnitTestCase): 20 | def setUp(self): 21 | super(TestMobileRedirectMiddleware, self).setUp() 22 | settings.MOBILE_DOMAIN = 'http://mobile.example.com/' 23 | self.middleware = MobileRedirectMiddleware() 24 | 25 | def tearDown(self): 26 | super(TestMobileRedirectMiddleware, self).tearDown() 27 | if hasattr(settings, 'MOBILE_REDIRECT_PRESERVE_URL'): 28 | del settings.MOBILE_REDIRECT_PRESERVE_URL 29 | 30 | def test_does_nothing_if_mobile_domain_not_set(self): 31 | settings.MOBILE_DOMAIN = None 32 | response = self.middleware.process_request(build_request('Mozilla/5.0 (PLAYSTATION 3; 1.00)')) 33 | self.assert_equals(None, response) 34 | 35 | def test_does_nothing_for_desktop_browser(self): 36 | self.assert_equals(None, self.middleware.process_request(build_request())) 37 | 38 | def test_does_nothing_if_isbrowser_cookie_set(self): 39 | response = self.middleware.process_request(build_request('Mozilla/5.0 (PLAYSTATION 3; 1.00)', {'isbrowser': '1'})) 40 | self.assert_equals(None, response) 41 | 42 | def test_sets_cookie_for_mobile_browser(self): 43 | response = self.middleware.process_request(build_request('Mozilla/5.0 (PLAYSTATION 3; 1.00)')) 44 | self.assert_true('ismobile' in response.cookies) 45 | self.assert_equals('1', response.cookies['ismobile'].value) 46 | 47 | def test_redirects_for_mobile_browser(self): 48 | response = self.middleware.process_request(build_request('Mozilla/5.0 (PLAYSTATION 3; 1.00)')) 49 | self.assert_true(isinstance(response, HttpResponseRedirect)) 50 | self.assert_equals(settings.MOBILE_DOMAIN, response['Location']) 51 | 52 | def test_redirects_if_ismobile_cookie_set(self): 53 | response = self.middleware.process_request(build_request(cookies={'ismobile': '1'})) 54 | self.assert_true(isinstance(response, HttpResponseRedirect)) 55 | self.assert_equals(settings.MOBILE_DOMAIN, response['Location']) 56 | 57 | def test_redirects_if_ismobile_cookie_set(self): 58 | settings.MOBILE_REDIRECT_PRESERVE_URL = True 59 | request = build_request(cookies={'ismobile': '1'}) 60 | request.path_info = '/some/url/' 61 | response = self.middleware.process_request(request) 62 | self.assert_true(isinstance(response, HttpResponseRedirect)) 63 | self.assert_equals(settings.MOBILE_DOMAIN + 'some/url/', response['Location']) 64 | --------------------------------------------------------------------------------