├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── funfactory ├── __init__.py ├── admin.py ├── cmd.py ├── context_processors.py ├── helpers.py ├── log.py ├── log_settings.py ├── manage.py ├── middleware.py ├── models.py ├── monkeypatches.py ├── requirements │ ├── compiled.txt │ ├── dev.txt │ └── prod.txt ├── settings_base.py ├── urlresolvers.py └── utils.py ├── setup.py ├── tests ├── __init__.py ├── run_tests.py ├── test__utils.py ├── test_accepted_locales.py ├── test_admin.py ├── test_context_processors.py ├── test_helpers.py ├── test_install.py ├── test_installer.sh ├── test_manage.py ├── test_middleware.py ├── test_migrations.py ├── test_settings.py └── test_urlresolvers.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = funfactory 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[po] 3 | .DS_Store 4 | .nose* 5 | .playdoh 6 | .tox 7 | .coverage 8 | dist 9 | funfactory.egg-info/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | before_script: 6 | - flake8 funfactory 7 | - mysql -e 'create database _funfactory_test;' 8 | script: FF_DB_USER=travis coverage run tests/run_tests.py 9 | install: 10 | - pip install -r funfactory/requirements/compiled.txt --use-mirrors 11 | - pip install -r funfactory/requirements/dev.txt --use-mirrors 12 | - pip install flake8 coverage --use-mirrors 13 | after_success: 14 | # Report coverage results to coveralls.io 15 | - pip install coveralls --use-mirrors 16 | - coveralls 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mozilla 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, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright owner nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include funfactory/requirements/ *.txt 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | funfactory is what makes `playdoh`_ fun. You import it within a Django 2 | `manage.py`_ file and it sets up the playdoh environment and configures some 3 | settings. 4 | 5 | .. image:: https://travis-ci.org/mozilla/funfactory.png 6 | :target: https://travis-ci.org/mozilla/funfactory 7 | .. image:: https://coveralls.io/repos/mozilla/funfactory/badge.png?branch=master 8 | :target: https://coveralls.io/r/mozilla/funfactory 9 | .. image:: https://pypip.in/v/funfactory/badge.png 10 | :target: https://crate.io/packages/funfactory 11 | 12 | Install 13 | ======= 14 | 15 | :: 16 | 17 | pip install funfactory 18 | 19 | What is it? 20 | =========== 21 | 22 | funfactory is the core of `playdoh`_, Mozilla's Django starter kit. 23 | funfactory is *not* a collection of standalone apps. 24 | 25 | Check out the `playdoh docs`_ for a complete user guide. 26 | 27 | funfactory is also the name of a script that automates the installation of a 28 | new Playdoh app. Check out ``funfactory --help`` for more info. 29 | 30 | .. _`playdoh`: https://github.com/mozilla/playdoh 31 | .. _`playdoh docs`: http://playdoh.readthedocs.org/ 32 | .. _`manage.py`: https://github.com/mozilla/playdoh/blob/master/manage.py 33 | 34 | Hacking 35 | ======= 36 | 37 | To develop new features for playdoh core, you'll want to hack on funfactory! 38 | To run the test suite, first install `tox`_ then cd into the root dir 39 | and type the ``tox`` command. The ``tox.ini`` will handle the rest. 40 | 41 | .. _`tox`: http://tox.readthedocs.org/ 42 | 43 | .. note:: 44 | if you supply a different playdoh remote URL or a different 45 | branch or something, remember to delete the ``.playdoh/`` directory 46 | between tests for a clean slate. 47 | 48 | To try out cutting edge funfactory features in a real playdoh app, you can use 49 | the develop command to install a link to the files within your virtualenv:: 50 | 51 | workon the-playdoh-clone 52 | pushd ~/your/path/to/funfactory 53 | python setup.py develop 54 | popd 55 | 56 | Test Suite Environment 57 | ====================== 58 | 59 | Here are some environment variables that are acknowledged by the test suite: 60 | 61 | **FF_DB_USER** 62 | MySQL db user to run manage.py test. Defaults to ``root``. 63 | 64 | **FF_DB_PASS** 65 | MySQL user password for manage.py test. Defaults to an empty string. 66 | 67 | **FF_DB_NAME** 68 | MySQL db name for manage.py test. Defaults to ``_funfactory_test``. 69 | 70 | **FF_DB_HOST** 71 | MySQL db host for manage.py test. Defaults to an empty string. 72 | 73 | **FF_PLAYDOH_REMOTE** 74 | Git qualified URL for playdoh repo. Defaults to ``git://github.com/mozilla/playdoh.git``. 75 | 76 | **FF_PLAYDOH_BRANCH** 77 | Default branch to pull and update. Defaults to ``master``. 78 | -------------------------------------------------------------------------------- /funfactory/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.3.0' 2 | -------------------------------------------------------------------------------- /funfactory/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin as django_admin 2 | from django.contrib.admin.sites import AdminSite 3 | 4 | from session_csrf import anonymous_csrf 5 | 6 | 7 | class SessionCsrfAdminSite(AdminSite): 8 | """Custom admin site that handles login with session_csrf.""" 9 | 10 | def login(self, request, extra_context=None): 11 | @anonymous_csrf 12 | def call_parent_login(request, extra_context): 13 | return super(SessionCsrfAdminSite, self).login(request, 14 | extra_context) 15 | 16 | return call_parent_login(request, extra_context) 17 | 18 | 19 | # This is for sites that import this file directly. 20 | site = SessionCsrfAdminSite() 21 | 22 | 23 | def monkeypatch(): 24 | django_admin.site = site 25 | -------------------------------------------------------------------------------- /funfactory/cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | Installs a skeleton Django app based on Mozilla's Playdoh. 3 | 4 | 1. Clones the Playdoh repo 5 | 2. Renames the project module to your custom package name 6 | 3. Creates a virtualenv 7 | 4. Installs/compiles the requirements 8 | 5. Creates a local settings file 9 | Read more about it here: http://playdoh.readthedocs.org/ 10 | """ 11 | from contextlib import contextmanager 12 | from datetime import datetime 13 | import logging 14 | import optparse 15 | import os 16 | import re 17 | import shutil 18 | import subprocess 19 | import sys 20 | import textwrap 21 | 22 | 23 | allow_user_input = True 24 | verbose = True 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | def clone_repo(pkg, dest, repo, repo_dest, branch): 29 | """Clone the Playdoh repo into a custom path.""" 30 | git(['clone', '--recursive', '-b', branch, repo, repo_dest]) 31 | 32 | 33 | def init_pkg(pkg, repo_dest): 34 | """ 35 | Initializes a custom named package module. 36 | 37 | This works by replacing all instances of 'project' with a custom module 38 | name. 39 | """ 40 | vars = {'pkg': pkg} 41 | with dir_path(repo_dest): 42 | patch("""\ 43 | diff --git a/manage.py b/manage.py 44 | index 40ebb0a..cdfe363 100755 45 | --- a/manage.py 46 | +++ b/manage.py 47 | @@ -3,7 +3,7 @@ import os 48 | import sys 49 | 50 | # Edit this if necessary or override the variable in your environment. 51 | -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 52 | +os.environ.setdefault('DJANGO_SETTINGS_MODULE', '%(pkg)s.settings') 53 | 54 | try: 55 | # For local development in a virtualenv: 56 | diff --git a/project/settings/base.py b/project/settings/base.py 57 | index 312f280..c75e673 100644 58 | --- a/project/settings/base.py 59 | +++ b/project/settings/base.py 60 | @@ -7,7 +7,7 @@ from funfactory.settings_base import * 61 | # If you did not install Playdoh with the funfactory installer script 62 | # you may need to edit this value. See the docs about installing from a 63 | # clone. 64 | -PROJECT_MODULE = 'project' 65 | +PROJECT_MODULE = '%(pkg)s' 66 | 67 | # Bundles is a dictionary of two dictionaries, css and js, which list css files 68 | # and js files that can be bundled together by the minify app. 69 | diff --git a/setup.py b/setup.py 70 | index 58dbd93..9a38628 100644 71 | --- a/setup.py 72 | +++ b/setup.py 73 | @@ -3,7 +3,7 @@ import os 74 | from setuptools import setup, find_packages 75 | 76 | 77 | -setup(name='project', 78 | +setup(name='%(pkg)s', 79 | version='1.0', 80 | description='Django application.', 81 | long_description='', 82 | """ % vars) 83 | 84 | git(['mv', 'project', pkg]) 85 | git(['commit', '-a', '-m', 'Renamed project module to %s' % pkg]) 86 | 87 | 88 | def generate_key(byte_length): 89 | """Return a true random ascii string containing byte_length of randomness. 90 | 91 | The resulting key is suitable for cryptogrpahy. 92 | The key will be hex encoded which means it will be twice as long 93 | as byte_length, i.e. 40 random bytes yields an 80 byte string. 94 | 95 | byte_length must be at least 32. 96 | """ 97 | if byte_length < 32: # at least 256 bit 98 | raise ValueError('um, %s is probably not long enough for cryptography' 99 | % byte_length) 100 | return os.urandom(byte_length).encode('hex') 101 | 102 | 103 | def create_settings(pkg, repo_dest, db_user, db_name, db_password, db_host, 104 | db_port): 105 | """ 106 | Creates a local settings file out of the distributed template. 107 | 108 | This also fills in database settings and generates a secret key, etc. 109 | """ 110 | vars = {'pkg': pkg, 111 | 'db_user': db_user, 112 | 'db_name': db_name, 113 | 'db_password': db_password or '', 114 | 'db_host': db_host or '', 115 | 'db_port': db_port or '', 116 | 'hmac_date': datetime.now().strftime('%Y-%m-%d'), 117 | 'hmac_key': generate_key(32), 118 | 'secret_key': generate_key(32)} 119 | with dir_path(repo_dest): 120 | shutil.copyfile('%s/settings/local.py-dist' % pkg, 121 | '%s/settings/local.py' % pkg) 122 | patch("""\ 123 | --- a/%(pkg)s/settings/local.py 124 | +++ b/%(pkg)s/settings/local.py 125 | @@ -9,11 +9,11 @@ from . import base 126 | DATABASES = { 127 | 'default': { 128 | 'ENGINE': 'django.db.backends.mysql', 129 | - 'NAME': 'playdoh_app', 130 | - 'USER': 'root', 131 | - 'PASSWORD': '', 132 | - 'HOST': '', 133 | - 'PORT': '', 134 | + 'NAME': '%(db_name)s', 135 | + 'USER': '%(db_user)s', 136 | + 'PASSWORD': '%(db_password)s', 137 | + 'HOST': '%(db_host)s', 138 | + 'PORT': '%(db_port)s', 139 | 'OPTIONS': { 140 | 'init_command': 'SET storage_engine=InnoDB', 141 | 'charset' : 'utf8', 142 | @@ -51,14 +51,14 @@ DEV = True 143 | # Playdoh ships with Bcrypt+HMAC by default because it's the most secure. 144 | # To use bcrypt, fill in a secret HMAC key. It cannot be blank. 145 | HMAC_KEYS = { 146 | - #'2012-06-06': 'some secret', 147 | + '%(hmac_date)s': '%(hmac_key)s', 148 | } 149 | 150 | from django_sha2 import get_password_hashers 151 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 152 | 153 | # Make this unique, and don't share it with anybody. It cannot be blank. 154 | -SECRET_KEY = '' 155 | +SECRET_KEY = '%(secret_key)s' 156 | 157 | # Uncomment these to activate and customize Celery: 158 | # CELERY_ALWAYS_EAGER = False # required to activate celeryd 159 | """ % vars) 160 | 161 | 162 | def create_virtualenv(pkg, repo_dest, python): 163 | """Creates a virtualenv within which to install your new application.""" 164 | workon_home = os.environ.get('WORKON_HOME') 165 | venv_cmd = find_executable('virtualenv') 166 | python_bin = find_executable(python) 167 | if not python_bin: 168 | raise EnvironmentError('%s is not installed or not ' 169 | 'available on your $PATH' % python) 170 | if workon_home: 171 | # Can't use mkvirtualenv directly here because relies too much on 172 | # shell tricks. Simulate it: 173 | venv = os.path.join(workon_home, pkg) 174 | else: 175 | venv = os.path.join(repo_dest, '.virtualenv') 176 | if venv_cmd: 177 | if not verbose: 178 | log.info('Creating virtual environment in %r' % venv) 179 | args = ['--python', python_bin, venv] 180 | if not verbose: 181 | args.insert(0, '-q') 182 | subprocess.check_call([venv_cmd] + args) 183 | else: 184 | raise EnvironmentError('Could not locate the virtualenv. Install with ' 185 | 'pip install virtualenv.') 186 | return venv 187 | 188 | 189 | def install_reqs(venv, repo_dest): 190 | """Installs all compiled requirements that can't be shipped in vendor.""" 191 | with dir_path(repo_dest): 192 | args = ['-r', 'requirements/compiled.txt'] 193 | if not verbose: 194 | args.insert(0, '-q') 195 | subprocess.check_call([os.path.join(venv, 'bin', 'pip'), 'install'] + 196 | args) 197 | 198 | 199 | def find_executable(name): 200 | """ 201 | Finds the actual path to a named command. 202 | 203 | The first one on $PATH wins. 204 | """ 205 | for pt in os.environ.get('PATH', '').split(':'): 206 | candidate = os.path.join(pt, name) 207 | if os.path.exists(candidate): 208 | return candidate 209 | 210 | 211 | def patch(hunk): 212 | args = ['-p1', '-r', '.'] 213 | if not verbose: 214 | args.insert(0, '--quiet') 215 | ps = subprocess.Popen(['patch'] + args, stdin=subprocess.PIPE) 216 | ps.stdin.write(textwrap.dedent(hunk)) 217 | ps.stdin.close() 218 | rs = ps.wait() 219 | if rs != 0: 220 | raise RuntimeError('patch %s returned non-zeo exit ' 221 | 'status %s' % (file, rs)) 222 | 223 | 224 | @contextmanager 225 | def dir_path(dir): 226 | """with dir_path(path) to change into a directory.""" 227 | old_dir = os.getcwd() 228 | os.chdir(dir) 229 | yield 230 | os.chdir(old_dir) 231 | 232 | 233 | def git(cmd_args): 234 | args = ['git'] 235 | cmd = cmd_args.pop(0) 236 | args.append(cmd) 237 | if not verbose: 238 | if cmd != 'mv': # doh 239 | args.append('--quiet') 240 | args.extend(cmd_args) 241 | if verbose: 242 | log.info(' '.join(args)) 243 | subprocess.check_call(args) 244 | 245 | 246 | def resolve_opt(opt, prompt): 247 | if not opt: 248 | if not allow_user_input: 249 | raise ValueError('%s (value was not set, using --no-input)' 250 | % prompt) 251 | opt = raw_input(prompt) 252 | return opt 253 | 254 | 255 | def main(): 256 | global allow_user_input, verbose 257 | ps = optparse.OptionParser(usage='%prog [options]\n' + __doc__) 258 | ps.add_option('-p', '--pkg', help='Name of your top level project package.') 259 | ps.add_option('-d', '--dest', 260 | help='Destination dir to put your new app. ' 261 | 'Default: %default', 262 | default=os.getcwd()) 263 | ps.add_option('-r', '--repo', 264 | help='Playdoh repository to clone. Default: %default', 265 | default='git://github.com/mozilla/playdoh.git') 266 | ps.add_option('-b', '--branch', 267 | help='Repository branch to clone. Default: %default', 268 | default='master') 269 | ps.add_option('--repo-dest', 270 | help='Clone repository into this directory. ' 271 | 'Default: DEST/PKG') 272 | ps.add_option('--venv', 273 | help='Path to an existing virtualenv you want to use. ' 274 | 'Otherwise, a new one will be created for you.') 275 | ps.add_option('-P', '--python', 276 | help='Python interpreter to use in your virtualenv. ' 277 | 'Default: which %default', 278 | default='python') 279 | ps.add_option('--db-user', 280 | help='Database user of your new app. Default: %default', 281 | default='root') 282 | ps.add_option('--db-name', 283 | help='Database name for your new app. Default: %default', 284 | default='playdoh_app') 285 | ps.add_option('--db-password', 286 | help='Database user password. Default: %default', 287 | default=None) 288 | ps.add_option('--db-host', 289 | help='Database connection host. Default: %default', 290 | default=None) 291 | ps.add_option('--db-port', 292 | help='Database connection port. Default: %default', 293 | default=None) 294 | ps.add_option('--no-input', help='Never prompt for user input', 295 | action='store_true', default=False) 296 | ps.add_option('-q', '--quiet', help='Less output', 297 | action='store_true', default=False) 298 | (options, args) = ps.parse_args() 299 | 300 | logging.basicConfig(level=logging.INFO, format="%(message)s", 301 | stream=sys.stdout) 302 | allow_user_input = not options.no_input 303 | verbose = not options.quiet 304 | options.pkg = resolve_opt(options.pkg, 'Top level package name: ') 305 | if not re.match('[a-zA-Z0-9_]+', options.pkg): 306 | ps.error('Package name %r can only contain letters, numbers, and ' 307 | 'underscores' % options.pkg) 308 | if not find_executable('mysql_config'): 309 | ps.error('Cannot find mysql_config. Please install MySQL!') 310 | if not options.repo_dest: 311 | options.repo_dest = os.path.abspath(os.path.join(options.dest, 312 | options.pkg)) 313 | clone_repo(options.pkg, options.dest, options.repo, options.repo_dest, 314 | options.branch) 315 | if options.venv: 316 | venv = options.venv 317 | elif os.environ.get('VIRTUAL_ENV'): 318 | venv = os.environ['VIRTUAL_ENV'] 319 | log.info('Using existing virtualenv in %s' % venv) 320 | else: 321 | venv = create_virtualenv(options.pkg, options.repo_dest, options.python) 322 | install_reqs(venv, options.repo_dest) 323 | init_pkg(options.pkg, options.repo_dest) 324 | create_settings(options.pkg, options.repo_dest, options.db_user, 325 | options.db_name, options.db_password, options.db_host, 326 | options.db_port) 327 | if verbose: 328 | log.info('') 329 | log.info('Aww yeah. Just installed you some Playdoh.') 330 | log.info('') 331 | log.info('cd %s' % options.repo_dest) 332 | if os.environ.get('WORKON_HOME'): 333 | log.info('workon %s' % options.pkg) 334 | else: 335 | log.info('source %s/bin/activate' 336 | % venv.replace(options.repo_dest, '.')) 337 | log.info('python manage.py runserver') 338 | log.info('') 339 | 340 | 341 | if __name__ == '__main__': 342 | main() 343 | -------------------------------------------------------------------------------- /funfactory/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils import translation 3 | 4 | 5 | def i18n(request): 6 | return {'LANGUAGES': settings.LANGUAGES, 7 | 'LANG': settings.LANGUAGE_URL_MAP.get(translation.get_language()) 8 | or translation.get_language(), 9 | 'DIR': 'rtl' if translation.get_language_bidi() else 'ltr', 10 | } 11 | 12 | 13 | def globals(request): 14 | return {'request': request, 15 | 'settings': settings} 16 | -------------------------------------------------------------------------------- /funfactory/helpers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import urllib 3 | import urlparse 4 | 5 | from django.contrib.staticfiles.storage import staticfiles_storage 6 | from django.template import defaultfilters 7 | from django.utils.encoding import smart_str 8 | from django.utils.html import strip_tags 9 | 10 | from jingo import register 11 | import jinja2 12 | 13 | from .urlresolvers import reverse 14 | 15 | # Yanking filters from Django. 16 | register.filter(strip_tags) 17 | register.filter(defaultfilters.timesince) 18 | register.filter(defaultfilters.truncatewords) 19 | 20 | 21 | @register.function 22 | def thisyear(): 23 | """The current year.""" 24 | return jinja2.Markup(datetime.date.today().year) 25 | 26 | 27 | @register.function 28 | def url(viewname, *args, **kwargs): 29 | """Helper for Django's ``reverse`` in templates.""" 30 | return reverse(viewname, args=args, kwargs=kwargs) 31 | 32 | 33 | @register.filter 34 | def urlparams(url_, hash=None, **query): 35 | """Add a fragment and/or query paramaters to a URL. 36 | 37 | New query params will be appended to exising parameters, except duplicate 38 | names, which will be replaced. 39 | """ 40 | url = urlparse.urlparse(url_) 41 | fragment = hash if hash is not None else url.fragment 42 | 43 | # Use dict(parse_qsl) so we don't get lists of values. 44 | q = url.query 45 | query_dict = dict(urlparse.parse_qsl(smart_str(q))) if q else {} 46 | query_dict.update((k, v) for k, v in query.items()) 47 | 48 | query_string = _urlencode([(k, v) for k, v in query_dict.items() 49 | if v is not None]) 50 | new = urlparse.ParseResult(url.scheme, url.netloc, url.path, url.params, 51 | query_string, fragment) 52 | return new.geturl() 53 | 54 | 55 | def _urlencode(items): 56 | """A Unicode-safe URLencoder.""" 57 | try: 58 | return urllib.urlencode(items) 59 | except UnicodeEncodeError: 60 | return urllib.urlencode([(k, smart_str(v)) for k, v in items]) 61 | 62 | 63 | @register.filter 64 | def urlencode(txt): 65 | """Url encode a path.""" 66 | if isinstance(txt, unicode): 67 | txt = txt.encode('utf-8') 68 | return urllib.quote_plus(txt) 69 | 70 | 71 | @register.function 72 | def static(path): 73 | return staticfiles_storage.url(path) 74 | -------------------------------------------------------------------------------- /funfactory/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.http import HttpRequest 5 | 6 | import commonware 7 | 8 | 9 | class AreciboHandler(logging.Handler): 10 | """An exception log handler that sends tracebacks to Arecibo.""" 11 | def emit(self, record): 12 | arecibo = getattr(settings, 'ARECIBO_SERVER_URL', '') 13 | 14 | if arecibo and hasattr(record, 'request'): 15 | if getattr(settings, 'ARECIBO_USES_CELERY', False): 16 | from django_arecibo.tasks import post 17 | else: 18 | from django_arecibo.wrapper import post 19 | post(record.request, 500) 20 | 21 | 22 | def log_cef(name, severity=logging.INFO, env=None, username='none', 23 | signature=None, **kwargs): 24 | """ 25 | Wraps cef logging function so we don't need to pass in the config 26 | dictionary every time. See bug 707060. ``env`` can be either a request 27 | object or just the request.META dictionary. 28 | """ 29 | 30 | cef_logger = commonware.log.getLogger('cef') 31 | 32 | c = {'product': settings.CEF_PRODUCT, 33 | 'vendor': settings.CEF_VENDOR, 34 | 'version': settings.CEF_VERSION, 35 | 'device_version': settings.CEF_DEVICE_VERSION} 36 | 37 | # The CEF library looks for some things in the env object like 38 | # REQUEST_METHOD and any REMOTE_ADDR stuff. Django not only doesn't send 39 | # half the stuff you'd expect, but it specifically doesn't implement 40 | # readline on its FakePayload object so these things fail. I have no idea 41 | # if that's outdated code in Django or not, but andym made this 42 | # awesome less crappy so the tests will actually pass. 43 | # In theory, the last part of this if() will never be hit except in the 44 | # test runner. Good luck with that. 45 | if isinstance(env, HttpRequest): 46 | r = env.META.copy() 47 | elif isinstance(env, dict): 48 | r = env 49 | else: 50 | r = {} 51 | 52 | # Drop kwargs into CEF config array, then log. 53 | c['environ'] = r 54 | c.update({ 55 | 'username': username, 56 | 'signature': signature, 57 | 'data': kwargs, 58 | }) 59 | 60 | cef_logger.log(severity, name, c) 61 | -------------------------------------------------------------------------------- /funfactory/log_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import socket 4 | 5 | from django.conf import settings 6 | 7 | import commonware.log 8 | import cef 9 | import dictconfig 10 | 11 | 12 | class NullHandler(logging.Handler): 13 | def emit(self, record): 14 | pass 15 | 16 | 17 | base_fmt = ('%(name)s:%(levelname)s %(message)s ' 18 | ':%(pathname)s:%(lineno)s') 19 | use_syslog = settings.HAS_SYSLOG and not settings.DEBUG 20 | 21 | if use_syslog: 22 | hostname = socket.gethostname() 23 | else: 24 | hostname = 'localhost' 25 | 26 | cfg = { 27 | 'version': 1, 28 | 'filters': {}, 29 | 'formatters': { 30 | 'debug': { 31 | '()': commonware.log.Formatter, 32 | 'datefmt': '%H:%M:%s', 33 | 'format': '%(asctime)s ' + base_fmt, 34 | }, 35 | 'prod': { 36 | '()': commonware.log.Formatter, 37 | 'datefmt': '%H:%M:%s', 38 | 'format': '%s %s: [%%(REMOTE_ADDR)s] %s' % (hostname, 39 | settings.SYSLOG_TAG, 40 | base_fmt), 41 | }, 42 | 'cef': { 43 | '()': cef.SysLogFormatter, 44 | 'datefmt': '%H:%M:%s', 45 | }, 46 | }, 47 | 'handlers': { 48 | 'console': { 49 | '()': logging.StreamHandler, 50 | 'formatter': 'debug', 51 | }, 52 | 'syslog': { 53 | '()': logging.handlers.SysLogHandler, 54 | 'facility': logging.handlers.SysLogHandler.LOG_LOCAL7, 55 | 'formatter': 'prod', 56 | }, 57 | 'arecibo': { 58 | 'level': 'ERROR', 59 | 'class': 'funfactory.log.AreciboHandler', 60 | }, 61 | 'mail_admins': { 62 | 'level': 'ERROR', 63 | 'class': 'django.utils.log.AdminEmailHandler', 64 | }, 65 | 'cef_syslog': { 66 | '()': logging.handlers.SysLogHandler, 67 | 'facility': logging.handlers.SysLogHandler.LOG_LOCAL4, 68 | 'formatter': 'cef', 69 | }, 70 | 'cef_console': { 71 | '()': logging.StreamHandler, 72 | 'formatter': 'cef', 73 | }, 74 | 'null': { 75 | '()': NullHandler, 76 | } 77 | }, 78 | 'loggers': { 79 | 'django.request': { 80 | # 'handlers': ['mail_admins', 'arecibo'], 81 | 'handlers': ['mail_admins', 'arecibo'], 82 | 'level': 'ERROR', 83 | 'propagate': False, 84 | }, 85 | 'cef': { 86 | 'handlers': ['cef_syslog' if use_syslog else 'cef_console'], 87 | } 88 | }, 89 | 'root': {}, 90 | } 91 | 92 | for key, value in settings.LOGGING.items(): 93 | if hasattr(cfg[key], 'update'): 94 | cfg[key].update(value) 95 | else: 96 | cfg[key] = value 97 | 98 | # Set the level and handlers for all loggers. 99 | for logger in cfg['loggers'].values() + [cfg['root']]: 100 | if 'handlers' not in logger: 101 | logger['handlers'] = ['syslog' if use_syslog else 'console'] 102 | if 'level' not in logger: 103 | logger['level'] = settings.LOG_LEVEL 104 | if logger is not cfg['root'] and 'propagate' not in logger: 105 | logger['propagate'] = False 106 | 107 | dictconfig.dictConfig(cfg) 108 | -------------------------------------------------------------------------------- /funfactory/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import os 4 | import site 5 | import sys 6 | import warnings 7 | 8 | 9 | current_settings = None 10 | execute_from_command_line = None 11 | log = logging.getLogger(__name__) 12 | ROOT = None 13 | 14 | 15 | def path(*a): 16 | if ROOT is None: 17 | _not_setup() 18 | return os.path.join(ROOT, *a) 19 | 20 | 21 | def setup_environ(manage_file, settings=None, more_pythonic=False): 22 | """Sets up a Django app within a manage.py file. 23 | 24 | Keyword Arguments 25 | 26 | **settings** 27 | An imported settings module. Without this, playdoh tries to import 28 | these modules (in order): DJANGO_SETTINGS_MODULE, settings 29 | 30 | **more_pythonic** 31 | When True, does not do any path hackery besides adding the vendor dirs. 32 | This requires a newer Playdoh layout without top level apps, lib, etc. 33 | """ 34 | # sys is global to avoid undefined local 35 | global sys, current_settings, execute_from_command_line, ROOT 36 | 37 | ROOT = os.path.dirname(os.path.abspath(manage_file)) 38 | 39 | # Adjust the python path and put local packages in front. 40 | prev_sys_path = list(sys.path) 41 | 42 | # Make root application importable without the need for 43 | # python setup.py install|develop 44 | sys.path.append(ROOT) 45 | 46 | if not more_pythonic: 47 | warnings.warn("You're using an old-style Playdoh layout with a top " 48 | "level __init__.py and apps directories. This is error " 49 | "prone and fights the Zen of Python. " 50 | "See http://playdoh.readthedocs.org/en/latest/" 51 | "getting-started/upgrading.html") 52 | # Give precedence to your app's parent dir, which contains __init__.py 53 | sys.path.append(os.path.abspath(os.path.join(ROOT, os.pardir))) 54 | 55 | site.addsitedir(path('apps')) 56 | site.addsitedir(path('lib')) 57 | 58 | # Local (project) vendor library 59 | site.addsitedir(path('vendor-local')) 60 | site.addsitedir(path('vendor-local/lib/python')) 61 | 62 | # Global (upstream) vendor library 63 | site.addsitedir(path('vendor')) 64 | site.addsitedir(path('vendor/lib/python')) 65 | 66 | # Move the new items to the front of sys.path. (via virtualenv) 67 | new_sys_path = [] 68 | for item in list(sys.path): 69 | if item not in prev_sys_path: 70 | new_sys_path.append(item) 71 | sys.path.remove(item) 72 | sys.path[:0] = new_sys_path 73 | 74 | from django.core.management import execute_from_command_line # noqa 75 | if not settings: 76 | if 'DJANGO_SETTINGS_MODULE' in os.environ: 77 | settings = import_mod_by_name(os.environ['DJANGO_SETTINGS_MODULE']) 78 | elif os.path.isfile(os.path.join(ROOT, 'settings_local.py')): 79 | import settings_local as settings 80 | warnings.warn("Using settings_local.py is deprecated. See " 81 | "http://playdoh.readthedocs.org/en/latest/upgrading.html", 82 | DeprecationWarning) 83 | else: 84 | import settings 85 | current_settings = settings 86 | validate_settings(settings) 87 | 88 | 89 | def validate_settings(settings): 90 | """ 91 | Raise an error in prod if we see any insecure settings. 92 | 93 | This used to warn during development but that was changed in 94 | 71718bec324c2561da6cc3990c927ee87362f0f7 95 | """ 96 | from django.core.exceptions import ImproperlyConfigured 97 | if settings.SECRET_KEY == '': 98 | msg = 'settings.SECRET_KEY cannot be blank! Check your local settings' 99 | if not settings.DEBUG: 100 | raise ImproperlyConfigured(msg) 101 | 102 | if getattr(settings, 'SESSION_COOKIE_SECURE', None) is None: 103 | msg = ('settings.SESSION_COOKIE_SECURE should be set to True; ' 104 | 'otherwise, your session ids can be intercepted over HTTP!') 105 | if not settings.DEBUG: 106 | raise ImproperlyConfigured(msg) 107 | 108 | hmac = getattr(settings, 'HMAC_KEYS', {}) 109 | if not len(hmac.keys()): 110 | msg = 'settings.HMAC_KEYS cannot be empty! Check your local settings' 111 | if not settings.DEBUG: 112 | raise ImproperlyConfigured(msg) 113 | 114 | 115 | def import_mod_by_name(target): 116 | # stolen from mock :) 117 | components = target.split('.') 118 | import_path = components.pop(0) 119 | thing = __import__(import_path) 120 | 121 | for comp in components: 122 | import_path += ".%s" % comp 123 | thing = _dot_lookup(thing, comp, import_path) 124 | return thing 125 | 126 | 127 | def _dot_lookup(thing, comp, import_path): 128 | try: 129 | return getattr(thing, comp) 130 | except AttributeError: 131 | __import__(import_path) 132 | return getattr(thing, comp) 133 | 134 | 135 | def _not_setup(): 136 | raise EnvironmentError( 137 | 'setup_environ() has not been called for this process') 138 | 139 | 140 | def main(argv=None): 141 | if current_settings is None: 142 | _not_setup() 143 | argv = argv or sys.argv 144 | execute_from_command_line(argv) 145 | -------------------------------------------------------------------------------- /funfactory/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Taken from zamboni.amo.middleware. 3 | 4 | This is django-localeurl, but with mozilla style capital letters in 5 | the locale codes. 6 | """ 7 | 8 | import urllib 9 | from warnings import warn 10 | 11 | from django.conf import settings 12 | from django.http import HttpResponsePermanentRedirect 13 | from django.utils.encoding import smart_str 14 | 15 | import tower 16 | 17 | from . import urlresolvers 18 | from .helpers import urlparams 19 | 20 | 21 | class LocaleURLMiddleware(object): 22 | """ 23 | 1. Search for the locale. 24 | 2. Save it in the request. 25 | 3. Strip them from the URL. 26 | """ 27 | 28 | def __init__(self): 29 | if not settings.USE_I18N or not settings.USE_L10N: 30 | warn("USE_I18N or USE_L10N is False but LocaleURLMiddleware is " 31 | "loaded. Consider removing funfactory.middleware." 32 | "LocaleURLMiddleware from your MIDDLEWARE_CLASSES setting.") 33 | 34 | self.exempt_urls = getattr(settings, 'FF_EXEMPT_LANG_PARAM_URLS', ()) 35 | 36 | def _is_lang_change(self, request): 37 | """Return True if the lang param is present and URL isn't exempt.""" 38 | if 'lang' not in request.GET: 39 | return False 40 | 41 | return not any(request.path.endswith(url) for url in self.exempt_urls) 42 | 43 | def process_request(self, request): 44 | prefixer = urlresolvers.Prefixer(request) 45 | urlresolvers.set_url_prefix(prefixer) 46 | full_path = prefixer.fix(prefixer.shortened_path) 47 | 48 | if self._is_lang_change(request): 49 | # Blank out the locale so that we can set a new one. Remove lang 50 | # from the query params so we don't have an infinite loop. 51 | prefixer.locale = '' 52 | new_path = prefixer.fix(prefixer.shortened_path) 53 | query = dict((smart_str(k), request.GET[k]) for k in request.GET) 54 | query.pop('lang') 55 | return HttpResponsePermanentRedirect(urlparams(new_path, **query)) 56 | 57 | if full_path != request.path: 58 | query_string = request.META.get('QUERY_STRING', '') 59 | full_path = urllib.quote(full_path.encode('utf-8')) 60 | 61 | if query_string: 62 | full_path = '%s?%s' % (full_path, query_string) 63 | 64 | response = HttpResponsePermanentRedirect(full_path) 65 | 66 | # Vary on Accept-Language if we changed the locale 67 | old_locale = prefixer.locale 68 | new_locale, _ = urlresolvers.split_path(full_path) 69 | if old_locale != new_locale: 70 | response['Vary'] = 'Accept-Language' 71 | 72 | return response 73 | 74 | request.path_info = '/' + prefixer.shortened_path 75 | request.locale = prefixer.locale 76 | tower.activate(prefixer.locale) 77 | -------------------------------------------------------------------------------- /funfactory/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/funfactory/c9bbf1c534eaa15641265bc75fa87afca52b7dd6/funfactory/models.py -------------------------------------------------------------------------------- /funfactory/monkeypatches.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.conf import settings 3 | 4 | 5 | __all__ = ['patch'] 6 | 7 | 8 | # Idempotence! http://en.wikipedia.org/wiki/Idempotence 9 | _has_patched = False 10 | 11 | 12 | def patch(): 13 | global _has_patched 14 | if _has_patched: 15 | return 16 | 17 | # Import for side-effect: configures logging handlers. 18 | # pylint: disable-msg=W0611 19 | import log_settings # noqa 20 | 21 | # Monkey-patch django forms to avoid having to use Jinja2's |safe 22 | # everywhere. 23 | try: 24 | import jingo.monkey 25 | jingo.monkey.patch() 26 | except ImportError: 27 | # If we can't import jingo.monkey, then it's an older jingo, 28 | # so we go back to the old ways. 29 | import safe_django_forms 30 | safe_django_forms.monkeypatch() 31 | 32 | # Monkey-patch Django's csrf_protect decorator to use session-based CSRF 33 | # tokens: 34 | if 'session_csrf' in settings.INSTALLED_APPS: 35 | import session_csrf 36 | session_csrf.monkeypatch() 37 | from . import admin 38 | admin.monkeypatch() 39 | 40 | if 'compressor' in settings.INSTALLED_APPS: 41 | import jingo 42 | from compressor.contrib.jinja2ext import CompressorExtension 43 | jingo.env.add_extension(CompressorExtension) 44 | 45 | logging.debug("Note: funfactory monkey patches executed in %s" % __file__) 46 | 47 | # prevent it from being run again later 48 | _has_patched = True 49 | -------------------------------------------------------------------------------- /funfactory/requirements/compiled.txt: -------------------------------------------------------------------------------- 1 | MySQL-python==1.2.3c1 2 | Jinja2==2.5.5 3 | 4 | # for bcrypt passwords 5 | py-bcrypt==0.3 6 | -------------------------------------------------------------------------------- /funfactory/requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # This file pulls in everything a developer needs. If it's a basic package 2 | # needed to run the site, it belongs in requirements/prod.txt. If it's a 3 | # package for developers (testing, docs, etc.), it goes in this file. 4 | 5 | -r prod.txt 6 | 7 | # Documentation 8 | Sphinx==1.0.7 9 | 10 | # Testing 11 | nose==1.0.0 12 | mock==1.0.1 13 | -e git://github.com/jbalogh/django-nose.git#egg=django_nose 14 | -e git://github.com/jbalogh/test-utils.git#egg=test-utils 15 | 16 | # L10n 17 | translate-toolkit==1.8.0 18 | -------------------------------------------------------------------------------- /funfactory/requirements/prod.txt: -------------------------------------------------------------------------------- 1 | # Django stuff 2 | Django==1.4 3 | -e git://github.com/jbalogh/django-multidb-router.git#egg=django-multidb-router 4 | -e git://github.com/jsocol/django-cronjobs.git#egg=django-cronjobs 5 | 6 | # BrowserID 7 | django-browserid==0.7.1 8 | requests==0.13.0 9 | 10 | # Forms 11 | -e git://github.com/mozilla/happyforms.git#egg=happyforms 12 | 13 | # Templates 14 | -e git://github.com/jbalogh/jingo.git#egg=jingo 15 | -e git://github.com/jsocol/jingo-minify.git#egg=jingo-minify 16 | GitPython==0.1.7 17 | 18 | # Various tidbits 19 | -e git://github.com/jsocol/commonware.git#egg=commonware 20 | -e git://github.com/mozilla/nuggets.git#egg=nuggets 21 | 22 | # Security 23 | -e git://github.com/fwenzel/django-sha2.git#egg=django-sha2 24 | -e git://github.com/jsocol/bleach.git#egg=bleach 25 | -e git://github.com/mozilla/django-session-csrf.git#egg=django-session-csrf 26 | cef==0.3 27 | 28 | # Celery: Message queue 29 | celery 30 | django-celery 31 | 32 | # L10n 33 | Babel>=0.9.4 34 | -e git://github.com/clouserw/tower.git#egg=tower 35 | -e git://github.com/fwenzel/django-mozilla-product-details#egg=django-mozilla-product-details 36 | 37 | # Mobile 38 | -e git://github.com/jbalogh/django-mobility@644e0c1c58#egg=django-mobility 39 | -------------------------------------------------------------------------------- /funfactory/settings_base.py: -------------------------------------------------------------------------------- 1 | # Django settings file for a project based on the playdoh template. 2 | # import * into your settings_local.py 3 | import logging 4 | import os 5 | import socket 6 | 7 | from django.utils.functional import lazy 8 | 9 | from .manage import ROOT, path 10 | 11 | 12 | # For backwards compatability, (projects built based on cloning playdoh) 13 | # we still have to have a ROOT_URLCONF. 14 | # For new-style playdoh projects this will be overridden automatically 15 | # by the new installer 16 | ROOT_URLCONF = '%s.urls' % os.path.basename(ROOT) 17 | 18 | # Is this a dev instance? 19 | DEV = False 20 | 21 | DEBUG = False 22 | TEMPLATE_DEBUG = DEBUG 23 | 24 | ADMINS = () 25 | MANAGERS = ADMINS 26 | 27 | DATABASES = {} # See settings_local. 28 | 29 | SLAVE_DATABASES = [] 30 | 31 | DATABASE_ROUTERS = ('multidb.PinningMasterSlaveRouter',) 32 | 33 | # Site ID is used by Django's Sites framework. 34 | SITE_ID = 1 35 | 36 | # Logging 37 | LOG_LEVEL = logging.INFO 38 | HAS_SYSLOG = True 39 | SYSLOG_TAG = "http_app_playdoh" # Change this after you fork. 40 | LOGGING_CONFIG = None 41 | LOGGING = {} 42 | 43 | # CEF Logging 44 | CEF_PRODUCT = 'Playdoh' 45 | CEF_VENDOR = 'Mozilla' 46 | CEF_VERSION = '0' 47 | CEF_DEVICE_VERSION = '0' 48 | 49 | 50 | # Internationalization. 51 | 52 | # Local time zone for this installation. Choices can be found here: 53 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 54 | # although not all choices may be available on all operating systems. 55 | # On Unix systems, a value of None will cause Django to use the same 56 | # timezone as the operating system. 57 | # If running in a Windows environment this must be set to the same as your 58 | # system time zone. 59 | TIME_ZONE = 'America/Los_Angeles' 60 | 61 | # If you set this to False, Django will make some optimizations so as not 62 | # to load the internationalization machinery. 63 | USE_I18N = True 64 | 65 | # If you set this to False, Django will not format dates, numbers and 66 | # calendars according to the current locale 67 | USE_L10N = True 68 | 69 | # Gettext text domain 70 | TEXT_DOMAIN = 'messages' 71 | STANDALONE_DOMAINS = [TEXT_DOMAIN, 'javascript'] 72 | TOWER_KEYWORDS = {'_lazy': None} 73 | TOWER_ADD_HEADERS = True 74 | 75 | # Language code for this installation. All choices can be found here: 76 | # http://www.i18nguy.com/unicode/language-identifiers.html 77 | LANGUAGE_CODE = 'en-US' 78 | 79 | # Accepted locales 80 | 81 | # Tells the product_details module where to find our local JSON files. 82 | # This ultimately controls how LANGUAGES are constructed. 83 | PROD_DETAILS_DIR = path('lib/product_details_json') 84 | 85 | # On dev instances, the list of accepted locales defaults to the contents of 86 | # the `locale` directory within a project module or, for older Playdoh apps, 87 | # the root locale directory. A localizer can add their locale in the l10n 88 | # repository (copy of which is checked out into `locale`) in order to start 89 | # testing the localization on the dev server. 90 | import glob 91 | import itertools 92 | DEV_LANGUAGES = None 93 | try: 94 | DEV_LANGUAGES = [ 95 | os.path.basename(loc).replace('_', '-') 96 | for loc in itertools.chain(glob.iglob(ROOT + '/locale/*'), # old style 97 | glob.iglob(ROOT + '/*/locale/*')) 98 | if (os.path.isdir(loc) and os.path.basename(loc) != 'templates') 99 | ] 100 | except OSError: 101 | pass 102 | 103 | # If the locale/ directory isn't there or it's empty, we make sure that 104 | # we always have at least 'en-US'. 105 | if not DEV_LANGUAGES: 106 | DEV_LANGUAGES = ('en-US',) 107 | 108 | # On stage/prod, the list of accepted locales is manually maintained. Only 109 | # locales whose localizers have signed off on their work should be listed here. 110 | PROD_LANGUAGES = ( 111 | 'en-US', 112 | ) 113 | 114 | # Map short locale names to long, preferred locale names. This will be used in 115 | # urlresolvers to determine the best-matching locale from the user's 116 | # Accept-Language header. 117 | CANONICAL_LOCALES = { 118 | 'en': 'en-US', 119 | } 120 | 121 | 122 | def lazy_lang_url_map(): 123 | from django.conf import settings 124 | langs = settings.DEV_LANGUAGES if settings.DEV else settings.PROD_LANGUAGES 125 | return dict([(i.lower(), i) for i in langs]) 126 | 127 | LANGUAGE_URL_MAP = lazy(lazy_lang_url_map, dict)() 128 | 129 | 130 | # Override Django's built-in with our native names 131 | def lazy_langs(): 132 | from django.conf import settings 133 | from product_details import product_details 134 | langs = DEV_LANGUAGES if settings.DEV else settings.PROD_LANGUAGES 135 | return dict([(lang.lower(), product_details.languages[lang]['native']) 136 | for lang in langs if lang in product_details.languages]) 137 | 138 | LANGUAGES = lazy(lazy_langs, dict)() 139 | 140 | # Tells the extract script what files to look for L10n in and what function 141 | # handles the extraction. The Tower library expects this. 142 | DOMAIN_METHODS = { 143 | 'messages': [ 144 | # Searching apps dirs only exists for historic playdoh apps. 145 | # See playdoh's base settings for how message paths are set. 146 | ('apps/**.py', 147 | 'tower.management.commands.extract.extract_tower_python'), 148 | ('apps/**/templates/**.html', 149 | 'tower.management.commands.extract.extract_tower_template'), 150 | ('templates/**.html', 151 | 'tower.management.commands.extract.extract_tower_template'), 152 | ], 153 | } 154 | 155 | # Paths that don't require a locale code in the URL. 156 | SUPPORTED_NONLOCALES = ['media', 'static', 'admin'] 157 | 158 | 159 | # Media and templates. 160 | 161 | # Absolute path to the directory that holds media. 162 | # Example: "/home/media/media.lawrence.com/" 163 | MEDIA_ROOT = path('media') 164 | 165 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 166 | # trailing slash if there is a path component (optional in other cases). 167 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 168 | MEDIA_URL = '/media/' 169 | 170 | # Absolute path to the directory static files should be collected to. 171 | # Don't put anything in this directory yourself; store your static files 172 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 173 | # Example: "/home/media/media.lawrence.com/static/" 174 | STATIC_ROOT = path('static') 175 | 176 | # URL prefix for static files. 177 | # Example: "http://media.lawrence.com/static/" 178 | STATIC_URL = '/static/' 179 | 180 | # Make this unique, and don't share it with anybody. 181 | # Set this in your local settings which is not committed to version control. 182 | SECRET_KEY = '' 183 | 184 | # List of callables that know how to import templates from various sources. 185 | TEMPLATE_LOADERS = ( 186 | 'jingo.Loader', 187 | 'django.template.loaders.filesystem.Loader', 188 | 'django.template.loaders.app_directories.Loader', 189 | ) 190 | 191 | TEMPLATE_CONTEXT_PROCESSORS = ( 192 | 'django.contrib.auth.context_processors.auth', 193 | 'django.core.context_processors.debug', 194 | 'django.core.context_processors.media', 195 | 'django.core.context_processors.request', 196 | 'session_csrf.context_processor', 197 | 'django.contrib.messages.context_processors.messages', 198 | 'funfactory.context_processors.i18n', 199 | 'funfactory.context_processors.globals', 200 | # 'jingo_minify.helpers.build_ids', 201 | ) 202 | 203 | 204 | def get_template_context_processors(exclude=(), append=(), 205 | current={'processors': TEMPLATE_CONTEXT_PROCESSORS}): 206 | """ 207 | Returns TEMPLATE_CONTEXT_PROCESSORS without the processors listed in 208 | exclude and with the processors listed in append. 209 | 210 | The use of a mutable dict is intentional, in order to preserve the state of 211 | the TEMPLATE_CONTEXT_PROCESSORS tuple across multiple settings files. 212 | """ 213 | 214 | current['processors'] = tuple( 215 | [p for p in current['processors'] if p not in exclude] 216 | ) + tuple(append) 217 | 218 | return current['processors'] 219 | 220 | 221 | TEMPLATE_DIRS = ( 222 | path('templates'), 223 | ) 224 | 225 | # Storage of static files 226 | COMPRESS_ROOT = STATIC_ROOT 227 | COMPRESS_CSS_FILTERS = ( 228 | 'compressor.filters.css_default.CssAbsoluteFilter', 229 | 'compressor.filters.cssmin.CSSMinFilter' 230 | ) 231 | COMPRESS_PRECOMPILERS = ( 232 | # ('text/coffeescript', 'coffee --compile --stdio'), 233 | ('text/less', 'lessc {infile} {outfile}'), 234 | # ('text/x-sass', 'sass {infile} {outfile}'), 235 | # ('text/x-scss', 'sass --scss {infile} {outfile}'), 236 | ) 237 | 238 | STATICFILES_FINDERS = ( 239 | 'django.contrib.staticfiles.finders.FileSystemFinder', 240 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 241 | 'compressor.finders.CompressorFinder', 242 | ) 243 | 244 | 245 | def JINJA_CONFIG(): 246 | # import jinja2 247 | # from django.conf import settings 248 | # from caching.base import cache 249 | config = {'extensions': ['tower.template.i18n', 'jinja2.ext.do', 250 | 'jinja2.ext.with_', 'jinja2.ext.loopcontrols'], 251 | 'finalize': lambda x: x if x is not None else ''} 252 | # if 'memcached' in cache.scheme and not settings.DEBUG: 253 | # # We're passing the _cache object directly to jinja because 254 | # # Django can't store binary directly; it enforces unicode on it. 255 | # # Details: http://jinja.pocoo.org/2/documentation/api#bytecode-cache 256 | # # and in the errors you get when you try it the other way. 257 | # bc = jinja2.MemcachedBytecodeCache(cache._cache, 258 | # "%sj2:" % settings.CACHE_PREFIX) 259 | # config['cache_size'] = -1 # Never clear the cache 260 | # config['bytecode_cache'] = bc 261 | return config 262 | 263 | 264 | # Middlewares, apps, URL configs. 265 | 266 | MIDDLEWARE_CLASSES = ( 267 | 'funfactory.middleware.LocaleURLMiddleware', 268 | 'multidb.middleware.PinningRouterMiddleware', 269 | 'django.middleware.common.CommonMiddleware', 270 | 'django.contrib.sessions.middleware.SessionMiddleware', 271 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 272 | 'session_csrf.CsrfMiddleware', # Must be after auth middleware. 273 | 'django.contrib.messages.middleware.MessageMiddleware', 274 | 'commonware.middleware.FrameOptionsHeader', 275 | 'mobility.middleware.DetectMobileMiddleware', 276 | 'mobility.middleware.XMobileMiddleware', 277 | ) 278 | 279 | 280 | def get_middleware(exclude=(), append=(), 281 | current={'middleware': MIDDLEWARE_CLASSES}): 282 | """ 283 | Returns MIDDLEWARE_CLASSES without the middlewares listed in exclude and 284 | with the middlewares listed in append. 285 | 286 | The use of a mutable dict is intentional, in order to preserve the state of 287 | the MIDDLEWARE_CLASSES tuple across multiple settings files. 288 | """ 289 | 290 | current['middleware'] = tuple( 291 | [m for m in current['middleware'] if m not in exclude] 292 | ) + tuple(append) 293 | return current['middleware'] 294 | 295 | 296 | INSTALLED_APPS = ( 297 | # Local apps 298 | 'funfactory', # Content common to most playdoh-based apps. 299 | 'compressor', 300 | 301 | 'tower', # for ./manage.py extract (L10n) 302 | 'cronjobs', # for ./manage.py cron * cmd line tasks 303 | 'django_browserid', 304 | 305 | 306 | # Django contrib apps 307 | 'django.contrib.auth', 308 | 'django.contrib.contenttypes', 309 | 'django.contrib.sessions', 310 | 'django.contrib.staticfiles', 311 | # 'django.contrib.sites', 312 | # 'django.contrib.messages', 313 | # Uncomment the next line to enable the admin: 314 | # 'django.contrib.admin', 315 | # Uncomment the next line to enable admin documentation: 316 | # 'django.contrib.admindocs', 317 | 318 | # Third-party apps, patches, fixes 319 | 'commonware.response.cookies', 320 | 'djcelery', 321 | 'django_nose', 322 | 'session_csrf', 323 | 324 | # L10n 325 | 'product_details', 326 | ) 327 | 328 | 329 | def get_apps(exclude=(), append=(), current={'apps': INSTALLED_APPS}): 330 | """ 331 | Returns INSTALLED_APPS without the apps listed in exclude and with the apps 332 | listed in append. 333 | 334 | The use of a mutable dict is intentional, in order to preserve the state of 335 | the INSTALLED_APPS tuple across multiple settings files. 336 | """ 337 | 338 | current['apps'] = tuple( 339 | [a for a in current['apps'] if a not in exclude] 340 | ) + tuple(append) 341 | return current['apps'] 342 | 343 | # Path to Java. Used for compress_assets. 344 | JAVA_BIN = '/usr/bin/java' 345 | 346 | # Sessions 347 | # 348 | # By default, be at least somewhat secure with our session cookies. 349 | SESSION_COOKIE_HTTPONLY = True 350 | SESSION_COOKIE_SECURE = True 351 | 352 | # Auth 353 | # The first hasher in this list will be used for new passwords. 354 | # Any other hasher in the list can be used for existing passwords. 355 | # Playdoh ships with Bcrypt+HMAC by default because it's the most secure. 356 | # To use bcrypt, fill in a secret HMAC key in your local settings. 357 | BASE_PASSWORD_HASHERS = ( 358 | 'django_sha2.hashers.BcryptHMACCombinedPasswordVerifier', 359 | 'django_sha2.hashers.SHA512PasswordHasher', 360 | 'django_sha2.hashers.SHA256PasswordHasher', 361 | 'django.contrib.auth.hashers.SHA1PasswordHasher', 362 | 'django.contrib.auth.hashers.MD5PasswordHasher', 363 | 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', 364 | ) 365 | HMAC_KEYS = { # for bcrypt only 366 | # '2012-06-06': 'cheesecake', 367 | } 368 | 369 | from django_sha2 import get_password_hashers 370 | PASSWORD_HASHERS = get_password_hashers(BASE_PASSWORD_HASHERS, HMAC_KEYS) 371 | 372 | # Tests 373 | TEST_RUNNER = 'test_utils.runner.RadicalTestSuiteRunner' 374 | 375 | # Celery 376 | 377 | # True says to simulate background tasks without actually using celeryd. 378 | # Good for local development in case celeryd is not running. 379 | CELERY_ALWAYS_EAGER = True 380 | 381 | BROKER_CONNECTION_TIMEOUT = 0.1 382 | CELERY_RESULT_BACKEND = 'amqp' 383 | CELERY_IGNORE_RESULT = True 384 | CELERY_EAGER_PROPAGATES_EXCEPTIONS = True 385 | 386 | # Time in seconds before celery.exceptions.SoftTimeLimitExceeded is raised. 387 | # The task can catch that and recover but should exit ASAP. 388 | CELERYD_TASK_SOFT_TIME_LIMIT = 60 * 2 389 | 390 | # Arecibo 391 | # when ARECIBO_SERVER_URL is set, it can use celery or the regular wrapper 392 | ARECIBO_USES_CELERY = True 393 | 394 | # For absolute urls 395 | try: 396 | DOMAIN = socket.gethostname() 397 | except socket.error: 398 | DOMAIN = 'localhost' 399 | PROTOCOL = "http://" 400 | PORT = 80 401 | 402 | # django-mobility 403 | MOBILE_COOKIE = 'mobile' 404 | -------------------------------------------------------------------------------- /funfactory/urlresolvers.py: -------------------------------------------------------------------------------- 1 | from threading import local 2 | 3 | from django.conf import settings 4 | from django.core.urlresolvers import reverse as django_reverse 5 | from django.utils.encoding import iri_to_uri 6 | from django.utils.functional import lazy 7 | from django.utils.translation.trans_real import parse_accept_lang_header 8 | 9 | 10 | # Thread-local storage for URL prefixes. Access with (get|set)_url_prefix. 11 | _local = local() 12 | 13 | 14 | def set_url_prefix(prefix): 15 | """Set the ``prefix`` for the current thread.""" 16 | _local.prefix = prefix 17 | 18 | 19 | def get_url_prefix(): 20 | """Get the prefix for the current thread, or None.""" 21 | return getattr(_local, 'prefix', None) 22 | 23 | 24 | def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None): 25 | """Wraps Django's reverse to prepend the correct locale.""" 26 | prefixer = get_url_prefix() 27 | 28 | if prefixer: 29 | prefix = prefix or '/' 30 | url = django_reverse(viewname, urlconf, args, kwargs, prefix) 31 | if prefixer: 32 | url = prefixer.fix(url) 33 | 34 | # Ensure any unicode characters in the URL are escaped. 35 | return iri_to_uri(url) 36 | 37 | 38 | reverse_lazy = lazy(reverse, str) 39 | 40 | 41 | def find_supported(test): 42 | return [settings.LANGUAGE_URL_MAP[x] for 43 | x in settings.LANGUAGE_URL_MAP if 44 | x.split('-', 1)[0] == test.lower().split('-', 1)[0]] 45 | 46 | 47 | def split_path(path_): 48 | """ 49 | Split the requested path into (locale, path). 50 | 51 | locale will be empty if it isn't found. 52 | """ 53 | path = path_.lstrip('/') 54 | 55 | # Use partitition instead of split since it always returns 3 parts 56 | first, _, rest = path.partition('/') 57 | 58 | lang = first.lower() 59 | if lang in settings.LANGUAGE_URL_MAP: 60 | return settings.LANGUAGE_URL_MAP[lang], rest 61 | else: 62 | supported = find_supported(first) 63 | if len(supported): 64 | return supported[0], rest 65 | else: 66 | return '', path 67 | 68 | 69 | class Prefixer(object): 70 | 71 | def __init__(self, request): 72 | self.request = request 73 | split = split_path(request.path_info) 74 | self.locale, self.shortened_path = split 75 | 76 | def get_language(self): 77 | """ 78 | Return a locale code we support on the site using the 79 | user's Accept-Language header to determine which is best. This 80 | mostly follows the RFCs but read bug 439568 for details. 81 | """ 82 | if 'lang' in self.request.GET: 83 | lang = self.request.GET['lang'].lower() 84 | if lang in settings.LANGUAGE_URL_MAP: 85 | return settings.LANGUAGE_URL_MAP[lang] 86 | 87 | if self.request.META.get('HTTP_ACCEPT_LANGUAGE'): 88 | best = self.get_best_language( 89 | self.request.META['HTTP_ACCEPT_LANGUAGE']) 90 | if best: 91 | return best 92 | return settings.LANGUAGE_CODE 93 | 94 | def get_best_language(self, accept_lang): 95 | """Given an Accept-Language header, return the best-matching language.""" 96 | LUM = settings.LANGUAGE_URL_MAP 97 | langs = dict(LUM.items() + settings.CANONICAL_LOCALES.items()) 98 | # Add missing short locales to the list. This will automatically map 99 | # en to en-GB (not en-US), es to es-AR (not es-ES), etc. in alphabetical 100 | # order. To override this behavior, explicitly define a preferred locale 101 | # map with the CANONICAL_LOCALES setting. 102 | langs.update((k.split('-')[0], v) for k, v in LUM.items() if 103 | k.split('-')[0] not in langs) 104 | try: 105 | ranked = parse_accept_lang_header(accept_lang) 106 | except ValueError: # see https://code.djangoproject.com/ticket/21078 107 | return 108 | else: 109 | for lang, _ in ranked: 110 | lang = lang.lower() 111 | if lang in langs: 112 | return langs[lang] 113 | pre = lang.split('-')[0] 114 | if pre in langs: 115 | return langs[pre] 116 | 117 | def fix(self, path): 118 | path = path.lstrip('/') 119 | url_parts = [self.request.META['SCRIPT_NAME']] 120 | 121 | if path.partition('/')[0] not in settings.SUPPORTED_NONLOCALES: 122 | locale = self.locale if self.locale else self.get_language() 123 | url_parts.append(locale) 124 | 125 | url_parts.append(path) 126 | 127 | return '/'.join(url_parts) 128 | -------------------------------------------------------------------------------- /funfactory/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | 6 | log = logging.getLogger('funfactory') 7 | 8 | 9 | def absolutify(url): 10 | """Takes a URL and prepends the SITE_URL""" 11 | site_url = getattr(settings, 'SITE_URL', False) 12 | 13 | # If we don't define it explicitly 14 | if not site_url: 15 | protocol = settings.PROTOCOL 16 | hostname = settings.DOMAIN 17 | port = settings.PORT 18 | if (protocol, port) in (('https://', 443), ('http://', 80)): 19 | site_url = ''.join(map(str, (protocol, hostname))) 20 | else: 21 | site_url = ''.join(map(str, (protocol, hostname, ':', port))) 22 | 23 | return site_url + url 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | version = None 8 | for line in open('./funfactory/__init__.py'): 9 | m = re.search('__version__\s*=\s*(.*)', line) 10 | if m: 11 | version = m.group(1).strip()[1:-1] # quotes 12 | break 13 | assert version 14 | 15 | 16 | setup( 17 | name='funfactory', 18 | version=version, 19 | description="Mozilla's Django app skeleton.", 20 | long_description=open(os.path.join(os.path.dirname(__file__), 21 | 'README.rst')).read(), 22 | author='Kumar McMillan and contributors', 23 | author_email='', 24 | license="BSD License", 25 | url='https://github.com/mozilla/funfactory', 26 | include_package_data=True, 27 | classifiers = [ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Environment :: Web Environment', 30 | 'Framework :: Django', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Natural Language :: English', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2.6', 37 | 'Programming Language :: Python :: 2.7', 38 | ], 39 | packages=find_packages(exclude=['tests']), 40 | entry_points=""" 41 | [console_scripts] 42 | funfactory = funfactory.cmd:main 43 | """, 44 | install_requires=[]) 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import os 3 | import shutil 4 | from subprocess import check_call 5 | import sys 6 | 7 | from nose.plugins import Plugin 8 | 9 | from funfactory import manage 10 | 11 | 12 | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 13 | PLAYDOH_ROOT = '.playdoh' 14 | PLAYDOH = os.path.join(ROOT, PLAYDOH_ROOT, 'funtestapp') 15 | ENVIRONMENT_NOTE = os.path.join(ROOT, PLAYDOH_ROOT, 'last-env.txt') 16 | shell = partial(check_call, shell=True) 17 | DB_USER = os.environ.get('FF_DB_USER', 'root') 18 | DB_PASS = os.environ.get('FF_DB_PASS', '') 19 | DB_HOST = os.environ.get('FF_DB_HOST', '') 20 | DB_NAME = os.environ.get('FF_DB_NAME', '_funfactory_test') 21 | FF_PLAYDOH_REMOTE = os.environ.get('FF_PLAYDOH_REMOTE', 22 | 'git://github.com/mozilla/playdoh.git') 23 | FF_PLAYDOH_BRANCH = os.environ.get('FF_PLAYDOH_BRANCH', 'master') 24 | 25 | 26 | def test_root(): 27 | assert os.path.exists(os.path.join(ROOT, 'setup.py')), ( 28 | 'This does not appear to be the root dir: %s' % ROOT) 29 | 30 | 31 | class FunFactoryTests(Plugin): 32 | """Enables the fun factory test suite.""" 33 | __test__ = False # Nose: do not collect as test 34 | name = 'ff-tests' 35 | score = 999 # needs to execute early 36 | 37 | def options(self, parser, env=os.environ): 38 | super(FunFactoryTests, self).options(parser, env=env) 39 | self.parser = parser 40 | 41 | def configure(self, options, conf): 42 | super(FunFactoryTests, self).configure(options, conf) 43 | self.enabled = True # Enables the plugin without a cmd line flag 44 | self.options = options 45 | 46 | def _write_last_environment(self): 47 | with open(ENVIRONMENT_NOTE, 'w') as f: 48 | f.write(self._this_environment()) 49 | 50 | def _read_last_environment(self): 51 | return open(ENVIRONMENT_NOTE).read() 52 | 53 | def _this_environment(self): 54 | return FF_PLAYDOH_REMOTE + '\n' + FF_PLAYDOH_BRANCH + '\n' 55 | 56 | def begin(self): 57 | if os.path.exists(ENVIRONMENT_NOTE): 58 | if self._read_last_environment() != self._this_environment(): 59 | shutil.rmtree(PLAYDOH) 60 | 61 | if not os.path.exists(PLAYDOH): 62 | container = os.path.abspath(os.path.join(PLAYDOH, '..')) 63 | if not os.path.exists(container): 64 | os.mkdir(container) 65 | check_call(['git', 'clone', '--recursive', 66 | '--branch', FF_PLAYDOH_BRANCH, 67 | FF_PLAYDOH_REMOTE, 68 | PLAYDOH]) 69 | else: 70 | 71 | proj_sh = partial(shell, cwd=PLAYDOH) 72 | proj_sh('git pull origin %s' % FF_PLAYDOH_BRANCH) 73 | proj_sh('git submodule sync -q') 74 | proj_sh('git submodule update --init --recursive') 75 | 76 | self._write_last_environment() 77 | 78 | st = os.path.join(PLAYDOH, 'project', 'settings', 'local.py') 79 | if os.path.exists(st): 80 | os.unlink(st) 81 | shutil.copy(os.path.join(PLAYDOH, 'project', 'settings', 82 | 'local.py-dist'), 83 | st) 84 | 85 | with open(st, 'r') as f: 86 | new_st = f.read() 87 | new_st = new_st.replace("'USER': 'root'", 88 | "'USER': '%s'" % DB_USER) 89 | new_st = new_st.replace("'PASSWORD': ''", 90 | "'PASSWORD': '%s'" % DB_PASS) 91 | new_st = new_st.replace("'HOST': ''", 92 | "'HOST': '%s'" % DB_HOST) 93 | new_st = new_st.replace("'NAME': 'playdoh_app'", 94 | "'NAME': '%s'" % DB_NAME) 95 | new_st = new_st.replace("SECRET_KEY = ''", 96 | "SECRET_KEY = 'testinglolz'") 97 | new_st = new_st + "\nfrom . import base\nINSTALLED_APPS = list(base.INSTALLED_APPS) + " \ 98 | "['django.contrib.admin']\n" 99 | new_st = new_st + "\nSITE_URL = ''\n" 100 | 101 | with open(st, 'w') as f: 102 | f.write(new_st) 103 | 104 | extra = '' 105 | if DB_PASS: 106 | extra = '--password=%s' % DB_PASS 107 | if DB_HOST: 108 | extra += ' -h %s' % DB_HOST 109 | shell('mysql -u %s %s -e "create database if not exists %s"' 110 | % (DB_USER, extra, DB_NAME)) 111 | check_call([sys.executable, 'manage.py', 'syncdb', '--noinput'], 112 | cwd=PLAYDOH) 113 | 114 | # For in-process tests: 115 | wd = os.getcwd() 116 | os.chdir(PLAYDOH) # Simulate what happens in a real app. 117 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 118 | try: 119 | manage.setup_environ(os.path.join(PLAYDOH, 'manage.py')) 120 | finally: 121 | os.chdir(wd) 122 | # Puts path back to this dev version of funfactory: 123 | sys.path.insert(0, ROOT) 124 | 125 | # simulate what django does, which is to import the root urls.py 126 | # once everything has been set up (e.g. setup_environ()) 127 | from funfactory.monkeypatches import patch 128 | patch() 129 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import nose 5 | 6 | 7 | __test__ = False # Not a test to be collected by Nose itself. 8 | 9 | 10 | if __name__ == '__main__': 11 | sys.path.append(os.getcwd()) # Simulate running nosetests from the root. 12 | from tests import FunFactoryTests 13 | nose.main(addplugins=[FunFactoryTests()]) 14 | -------------------------------------------------------------------------------- /tests/test__utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from mock import patch 4 | from nose.tools import eq_ 5 | from django.test import TestCase 6 | 7 | import funfactory.utils as utils 8 | 9 | 10 | @patch.object(settings, 'DOMAIN', 'test.mo.com') 11 | class AbsolutifyTests(TestCase): 12 | ABS_PATH = '/some/absolute/path' 13 | 14 | @patch.object(settings, 'SITE_URL', 'http://testserver') 15 | def test_basic(self): 16 | url = utils.absolutify(AbsolutifyTests.ABS_PATH) 17 | eq_('%s/some/absolute/path' % settings.SITE_URL, url) 18 | 19 | @patch.object(settings, 'PROTOCOL', 'https://') 20 | @patch.object(settings, 'PORT', 443) 21 | @patch.object(settings, 'SITE_URL', 'http://testserver') 22 | def test_https(self): 23 | url = utils.absolutify(AbsolutifyTests.ABS_PATH) 24 | eq_('%s/some/absolute/path' % settings.SITE_URL, url) 25 | 26 | @patch.object(settings, 'SITE_URL', '') 27 | @patch.object(settings, 'PORT', 8009) 28 | def test_with_port(self): 29 | url = utils.absolutify(AbsolutifyTests.ABS_PATH) 30 | eq_('http://test.mo.com:8009/some/absolute/path', url) 31 | -------------------------------------------------------------------------------- /tests/test_accepted_locales.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | import test_utils 6 | 7 | from funfactory.manage import path 8 | 9 | 10 | class AcceptedLocalesTest(test_utils.TestCase): 11 | """Test lazy evaluation of locale related settings. 12 | 13 | Verify that some localization-related settings are lazily evaluated based 14 | on the current value of the DEV variable. Depending on the value, 15 | DEV_LANGUAGES or PROD_LANGUAGES should be used. 16 | 17 | """ 18 | locale = path('project/locale') 19 | locale_bkp = path('project/locale_bkp') 20 | 21 | @classmethod 22 | def setup_class(cls): 23 | """Create a directory structure for locale/. 24 | 25 | Back up the existing project/locale/ directory and create the following 26 | hierarchy in its place: 27 | 28 | - project/locale/en-US/LC_MESSAGES 29 | - project/locale/fr/LC_MESSAGES 30 | - project/locale/templates/LC_MESSAGES 31 | - project/locale/empty_file 32 | 33 | Also, set PROD_LANGUAGES to ('en-US',). 34 | 35 | """ 36 | if os.path.exists(cls.locale_bkp): 37 | raise Exception('A backup of locale/ exists at %s which might ' 38 | 'mean that previous tests didn\'t end cleanly. ' 39 | 'Skipping the test suite.' % cls.locale_bkp) 40 | cls.DEV = settings.DEV 41 | cls.PROD_LANGUAGES = settings.PROD_LANGUAGES 42 | cls.DEV_LANGUAGES = settings.DEV_LANGUAGES 43 | settings.PROD_LANGUAGES = ('en-US',) 44 | os.rename(cls.locale, cls.locale_bkp) 45 | for loc in ('en-US', 'fr', 'templates'): 46 | os.makedirs(os.path.join(cls.locale, loc, 'LC_MESSAGES')) 47 | open(os.path.join(cls.locale, 'empty_file'), 'w').close() 48 | 49 | @classmethod 50 | def teardown_class(cls): 51 | """Remove the testing locale/ dir and bring back the backup.""" 52 | 53 | settings.DEV = cls.DEV 54 | settings.PROD_LANGUAGES = cls.PROD_LANGUAGES 55 | settings.DEV_LANGUAGES = cls.DEV_LANGUAGES 56 | shutil.rmtree(cls.locale) 57 | os.rename(cls.locale_bkp, cls.locale) 58 | 59 | def test_build_dev_languages(self): 60 | """Test that the list of dev locales is built properly. 61 | 62 | On dev instances, the list of accepted locales should correspond to 63 | the per-locale directories in locale/. 64 | 65 | """ 66 | settings.DEV = True 67 | assert (settings.DEV_LANGUAGES == ['en-US', 'fr'] or 68 | settings.DEV_LANGUAGES == ['fr', 'en-US']), \ 69 | 'DEV_LANGUAGES do not correspond to the contents of locale/.' 70 | 71 | def test_dev_languages(self): 72 | """Test the accepted locales on dev instances. 73 | 74 | On dev instances, allow locales defined in DEV_LANGUAGES. 75 | 76 | """ 77 | settings.DEV = True 78 | # simulate the successful result of the DEV_LANGUAGES list 79 | # comprehension defined in settings. 80 | settings.DEV_LANGUAGES = ['en-US', 'fr'] 81 | assert settings.LANGUAGE_URL_MAP == {'en-us': 'en-US', 'fr': 'fr'}, \ 82 | ('DEV is True, but DEV_LANGUAGES are not used to define the ' 83 | 'allowed locales.') 84 | 85 | def test_prod_languages(self): 86 | """Test the accepted locales on prod instances. 87 | 88 | On stage/prod instances, allow locales defined in PROD_LANGUAGES. 89 | 90 | """ 91 | settings.DEV = False 92 | assert settings.LANGUAGE_URL_MAP == {'en-us': 'en-US'}, \ 93 | ('DEV is False, but PROD_LANGUAGES are not used to define the ' 94 | 'allowed locales.') 95 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.defaults import patterns 3 | from django.contrib import admin 4 | import django.contrib.admin.sites 5 | from django.template.loader import BaseLoader 6 | from django.test import TestCase 7 | 8 | from mock import patch, Mock 9 | from session_csrf import ANON_COOKIE 10 | 11 | 12 | urlpatterns = None 13 | 14 | 15 | def setup(): 16 | global urlpatterns 17 | urlpatterns = patterns('', 18 | (r'^admin/$', admin.site.urls), 19 | ) 20 | 21 | 22 | class FakeLoader(BaseLoader): 23 | """ 24 | Gets around TemplateNotFound errors by always returning an empty string as 25 | the template. 26 | """ 27 | is_usable = True 28 | 29 | def load_template_source(self, template_name, template_dirs=None): 30 | return ('', 'FakeLoader') 31 | 32 | 33 | @patch.object(settings, 'TEMPLATE_LOADERS', ['tests.test_admin.FakeLoader']) 34 | @patch.object(settings, 'ALLOWED_HOSTS', ['testserver']) 35 | class SessionCsrfAdminTests(TestCase): 36 | urls = 'tests.test_admin' 37 | 38 | @patch.object(django.contrib.admin.sites, 'reverse') 39 | def test_login_has_csrf(self, reverse): 40 | reverse = Mock() 41 | self.client.get('admin/', follow=True) 42 | assert self.client.cookies.get(ANON_COOKIE), ( 43 | "Anonymous CSRF Cookie not set.") 44 | -------------------------------------------------------------------------------- /tests/test_context_processors.py: -------------------------------------------------------------------------------- 1 | import jingo 2 | import jinja2 3 | from nose.tools import eq_ 4 | from django.test import TestCase, RequestFactory 5 | 6 | from mock import patch 7 | 8 | import funfactory.context_processors 9 | 10 | 11 | class TestContext(TestCase): 12 | 13 | def setUp(self): 14 | self.factory = RequestFactory() 15 | 16 | def render(self, content, request=None): 17 | if not request: 18 | request = self.factory.get('/') 19 | tpl = jinja2.Template(content) 20 | return jingo.render_to_string(request, tpl) 21 | 22 | def test_request(self): 23 | eq_(self.render('{{ request.path }}'), '/') 24 | 25 | def test_settings(self): 26 | eq_(self.render('{{ settings.SITE_ID }}'), '1') 27 | 28 | def test_languages(self): 29 | eq_(self.render("{{ LANGUAGES['en-us'] }}"), 'English (US)') 30 | 31 | @patch.object(funfactory.context_processors, 'translation') 32 | def test_languages(self, translation): 33 | translation.get_language.return_value = 'en-US' 34 | eq_(self.render("{{ LANG }}"), 'en-US') 35 | 36 | def test_lang_dir(self): 37 | eq_(self.render("{{ DIR }}"), 'ltr') 38 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | from django.test import TestCase 3 | import jingo 4 | 5 | 6 | def render(s, context={}): 7 | t = jingo.env.from_string(s) 8 | return t.render(context) 9 | 10 | 11 | class HelpersTests(TestCase): 12 | 13 | def test_urlencode_with_unicode(self): 14 | template = '' 15 | context = {'key': '?& /()'} 16 | eq_(render(template, context), '') 17 | # non-ascii 18 | context = {'key': u'\xe4'} 19 | eq_(render(template, context), '') 20 | -------------------------------------------------------------------------------- /tests/test_install.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | from subprocess import Popen 4 | import unittest 5 | 6 | from nose.tools import eq_ 7 | 8 | from tests import PLAYDOH 9 | 10 | 11 | class TestInstall(unittest.TestCase): 12 | 13 | def test(self): 14 | # sys.executable is our tox virtualenv that includes 15 | # compiled/dev modules. 16 | p = Popen([sys.executable, 'manage.py', 'test'], 17 | stderr=subprocess.STDOUT, stdout=subprocess.PIPE, 18 | cwd=PLAYDOH) 19 | print p.stdout.read() 20 | eq_(p.wait(), 0) 21 | -------------------------------------------------------------------------------- /tests/test_installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a smoke test to check the installer from Jenkins CI. 3 | rm -fr installer_test 4 | ./.tox/py26/bin/funfactory --no-input --pkg installer_test 5 | exit $? 6 | -------------------------------------------------------------------------------- /tests/test_manage.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import xml.dom 3 | import unittest 4 | 5 | from nose.tools import eq_, raises 6 | 7 | from funfactory.manage import import_mod_by_name 8 | 9 | 10 | class TestImporter(unittest.TestCase): 11 | 12 | def test_single_mod(self): 13 | eq_(import_mod_by_name('smtplib'), smtplib) 14 | 15 | def test_mod_attr(self): 16 | eq_(import_mod_by_name('smtplib.SMTP'), smtplib.SMTP) 17 | 18 | def test_multiple_attrs(self): 19 | eq_(import_mod_by_name('smtplib.SMTP.connect'), 20 | smtplib.SMTP.connect) 21 | 22 | def test_multiple_mods(self): 23 | eq_(import_mod_by_name('xml.dom'), xml.dom) 24 | 25 | @raises(ImportError) 26 | def test_unknown_mod(self): 27 | import_mod_by_name('notthenameofamodule') 28 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, RequestFactory 2 | from django.test.utils import override_settings 3 | 4 | from funfactory.middleware import LocaleURLMiddleware 5 | 6 | 7 | class TestLocaleURLMiddleware(TestCase): 8 | def setUp(self): 9 | self.rf = RequestFactory() 10 | self.middleware = LocaleURLMiddleware() 11 | 12 | @override_settings(DEV_LANGUAGES=('de', 'fr'), 13 | FF_EXEMPT_LANG_PARAM_URLS=()) 14 | def test_redirects_to_correct_language(self): 15 | """Should redirect to lang prefixed url.""" 16 | path = '/the/dude/' 17 | req = self.rf.get(path, HTTP_ACCEPT_LANGUAGE='de') 18 | resp = LocaleURLMiddleware().process_request(req) 19 | self.assertEqual(resp['Location'], '/de' + path) 20 | 21 | @override_settings(DEV_LANGUAGES=('es', 'fr'), 22 | LANGUAGE_CODE='en-US', 23 | FF_EXEMPT_LANG_PARAM_URLS=()) 24 | def test_redirects_to_default_language(self): 25 | """Should redirect to default lang if not in settings.""" 26 | path = '/the/dude/' 27 | req = self.rf.get(path, HTTP_ACCEPT_LANGUAGE='de') 28 | resp = LocaleURLMiddleware().process_request(req) 29 | self.assertEqual(resp['Location'], '/en-US' + path) 30 | 31 | @override_settings(DEV_LANGUAGES=('de', 'fr'), 32 | FF_EXEMPT_LANG_PARAM_URLS=('/other/',)) 33 | def test_redirects_lang_param(self): 34 | """Middleware should remove the lang param on redirect.""" 35 | path = '/fr/the/dude/' 36 | req = self.rf.get(path, {'lang': 'de'}) 37 | resp = LocaleURLMiddleware().process_request(req) 38 | self.assertEqual(resp['Location'], '/de/the/dude/') 39 | 40 | @override_settings(DEV_LANGUAGES=('de', 'fr'), 41 | FF_EXEMPT_LANG_PARAM_URLS=('/dude/',)) 42 | def test_no_redirect_lang_param(self): 43 | """Middleware should not redirect when exempt.""" 44 | path = '/fr/the/dude/' 45 | req = self.rf.get(path, {'lang': 'de'}) 46 | resp = LocaleURLMiddleware().process_request(req) 47 | self.assertIs(resp, None) # no redirect 48 | -------------------------------------------------------------------------------- /tests/test_migrations.py: -------------------------------------------------------------------------------- 1 | import re 2 | from os import listdir 3 | from os.path import join 4 | 5 | from django.test import TestCase 6 | 7 | from funfactory.manage import path 8 | 9 | 10 | class MigrationTests(TestCase): 11 | """Sanity checks for the SQL migration scripts.""" 12 | 13 | @staticmethod 14 | def _migrations_path(): 15 | """Return the absolute path to the migration script folder.""" 16 | return path('migrations') 17 | 18 | def test_unique(self): 19 | """Assert that the numeric prefixes of the DB migrations are unique.""" 20 | leading_digits = re.compile(r'^\d+') 21 | seen_numbers = set() 22 | path = self._migrations_path() 23 | for filename in listdir(path): 24 | match = leading_digits.match(filename) 25 | if match: 26 | number = match.group() 27 | if number in seen_numbers: 28 | self.fail('There is more than one migration #%s in %s.' % 29 | (number, path)) 30 | seen_numbers.add(number) 31 | 32 | def test_innodb_and_utf8(self): 33 | """Make sure each created table uses the InnoDB engine and UTF-8.""" 34 | # Heuristic: make sure there are at least as many "ENGINE=InnoDB"s as 35 | # "CREATE TABLE"s. (There might be additional "InnoDB"s in ALTER TABLE 36 | # statements, which are fine.) 37 | path = self._migrations_path() 38 | for filename in sorted(listdir(path)): 39 | with open(join(path, filename)) as f: 40 | contents = f.read() 41 | creates = contents.count('CREATE TABLE') 42 | engines = contents.count('ENGINE=InnoDB') 43 | encodings = (contents.count('CHARSET=utf8') + 44 | contents.count('CHARACTER SET utf8')) 45 | assert engines >= creates, ("There weren't as many " 46 | 'occurrences of "ENGINE=InnoDB" as of "CREATE TABLE" in ' 47 | 'migration %s.' % filename) 48 | assert encodings >= creates, ("There weren't as many " 49 | 'UTF-8 declarations as "CREATE TABLE" occurrences in ' 50 | 'migration %s.' % filename) 51 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from mock import Mock, patch 5 | from nose.tools import eq_, raises 6 | 7 | from funfactory.manage import validate_settings 8 | from funfactory.settings_base import (get_apps, get_middleware, 9 | get_template_context_processors) 10 | 11 | 12 | @patch.object(settings, 'DEBUG', True) 13 | @patch.object(settings, 'HMAC_KEYS', {'2012-06-06': 'secret'}) 14 | @patch.object(settings, 'SECRET_KEY', 'any random value') 15 | @patch.object(settings, 'SESSION_COOKIE_SECURE', False) 16 | def test_insecure_session_cookie_for_dev(): 17 | validate_settings(settings) 18 | 19 | 20 | @raises(ImproperlyConfigured) 21 | @patch.object(settings, 'DEBUG', False) 22 | @patch.object(settings, 'HMAC_KEYS', {'2012-06-06': 'secret'}) 23 | @patch.object(settings, 'SECRET_KEY', '') 24 | @patch.object(settings, 'SESSION_COOKIE_SECURE', True) 25 | def test_empty_secret_key_for_prod(): 26 | validate_settings(settings) 27 | 28 | 29 | @patch.object(settings, 'DEBUG', False) 30 | @patch.object(settings, 'HMAC_KEYS', {'2012-06-06': 'secret'}) 31 | @patch.object(settings, 'SECRET_KEY', 'any random value') 32 | @patch.object(settings, 'SESSION_COOKIE_SECURE', True) 33 | def test_secret_key_ok(): 34 | """Validate required security-related settings. 35 | 36 | Don't raise exceptions when required settings are set properly.""" 37 | validate_settings(settings) 38 | 39 | 40 | @raises(ImproperlyConfigured) 41 | @patch.object(settings, 'DEBUG', False) 42 | @patch.object(settings, 'HMAC_KEYS', {'2012-06-06': 'secret'}) 43 | @patch.object(settings, 'SECRET_KEY', 'any random value') 44 | @patch.object(settings, 'SESSION_COOKIE_SECURE', None) 45 | def test_session_cookie_ok(): 46 | """Raise an exception if session cookies aren't secure in production.""" 47 | validate_settings(settings) 48 | 49 | 50 | @patch.object(settings, 'DEBUG', True) 51 | @patch.object(settings, 'HMAC_KEYS', {}) 52 | @patch.object(settings, 'SESSION_COOKIE_SECURE', False) 53 | def test_empty_hmac_in_dev(): 54 | # Should not raise an exception. 55 | validate_settings(settings) 56 | 57 | 58 | @raises(ImproperlyConfigured) 59 | @patch.object(settings, 'DEBUG', False) 60 | @patch.object(settings, 'HMAC_KEYS', {}) 61 | @patch.object(settings, 'SESSION_COOKIE_SECURE', False) 62 | def test_empty_hmac_in_prod(): 63 | validate_settings(settings) 64 | 65 | 66 | def test_get_apps(): 67 | eq_(get_apps(exclude=('chico',), 68 | current={'apps': ('groucho', 'harpo', 'chico')}), 69 | ('groucho', 'harpo')) 70 | eq_(get_apps(append=('zeppo',), 71 | current={'apps': ('groucho', 'harpo', 'chico')}), 72 | ('groucho', 'harpo', 'chico', 'zeppo')) 73 | eq_(get_apps(exclude=('harpo', 'zeppo'), append=('chico',), 74 | current={'apps': ('groucho', 'harpo', 'zeppo')}), 75 | ('groucho', 'chico')) 76 | eq_(get_apps(exclude=('funfactory'), append=('gummo',)), get_apps()) 77 | 78 | 79 | def test_get_middleware(): 80 | eq_(get_middleware(exclude=['larry', 'moe'], 81 | current={'middleware': ('larry', 'curly', 'moe')}), 82 | ('curly',)) 83 | eq_(get_middleware(append=('shemp', 'moe'), 84 | current={'middleware': ('larry', 'curly')}), 85 | ('larry', 'curly', 'shemp', 'moe')) 86 | eq_(get_middleware(exclude=('curly'), append=['moe'], 87 | current={'middleware': ('shemp', 'curly', 'larry')}), 88 | ('shemp', 'larry', 'moe')) 89 | eq_(get_middleware(append=['emil']), get_middleware()) 90 | 91 | 92 | def test_get_processors(): 93 | eq_(get_template_context_processors(exclude=('aramis'), 94 | current={'processors': ('athos', 'porthos', 'aramis')}), 95 | ('athos', 'porthos')) 96 | eq_(get_template_context_processors(append=("d'artagnan",), 97 | current={'processors': ('athos', 'porthos')}), 98 | ('athos', 'porthos', "d'artagnan")) 99 | eq_(get_template_context_processors(exclude=['athos'], append=['aramis'], 100 | current={'processors': ('athos', 'porthos', "d'artagnan")}), 101 | ('porthos', "d'artagnan", 'aramis')) 102 | eq_(get_template_context_processors(append=['richelieu']), 103 | get_template_context_processors()) 104 | -------------------------------------------------------------------------------- /tests/test_urlresolvers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | from django.conf.urls.defaults import patterns, url 4 | from django.test import TestCase 5 | from django.test.client import RequestFactory 6 | from django.test.utils import override_settings 7 | 8 | from funfactory.urlresolvers import reverse, split_path, Prefixer 9 | from mock import patch, Mock 10 | from nose.tools import eq_, ok_ 11 | 12 | 13 | # split_path tests use a test generator, which cannot be used inside of a 14 | # TestCase class 15 | def test_split_path(): 16 | testcases = [ 17 | # Basic 18 | ('en-US/some/action', ('en-US', 'some/action')), 19 | # First slash doesn't matter 20 | ('/en-US/some/action', ('en-US', 'some/action')), 21 | # Nor does capitalization 22 | ('En-uS/some/action', ('en-US', 'some/action')), 23 | # Unsupported languages return a blank language 24 | ('unsupported/some/action', ('', 'unsupported/some/action')), 25 | ] 26 | 27 | for tc in testcases: 28 | yield check_split_path, tc[0], tc[1] 29 | 30 | 31 | def check_split_path(path, result): 32 | res = split_path(path) 33 | eq_(res, result) 34 | 35 | 36 | # Test urlpatterns 37 | urlpatterns = patterns('', 38 | url(r'^test/$', lambda r: None, name='test.view') 39 | ) 40 | 41 | 42 | class FakePrefixer(object): 43 | def __init__(self, fix): 44 | self.fix = fix 45 | 46 | 47 | @patch('funfactory.urlresolvers.get_url_prefix') 48 | class TestReverse(TestCase): 49 | urls = 'tests.test_urlresolvers' 50 | 51 | def test_unicode_url(self, get_url_prefix): 52 | # If the prefixer returns a unicode URL it should be escaped and cast 53 | # as a str object. 54 | get_url_prefix.return_value = FakePrefixer(lambda p: u'/Françoi%s' % p) 55 | result = reverse('test.view') 56 | 57 | # Ensure that UTF-8 characters are escaped properly. 58 | self.assertEqual(result, '/Fran%C3%A7oi/test/') 59 | self.assertEqual(type(result), str) 60 | 61 | 62 | class TestPrefixer(TestCase): 63 | def setUp(self): 64 | self.factory = RequestFactory() 65 | 66 | @override_settings(LANGUAGE_CODE='en-US') 67 | def test_get_language_default_language_code(self): 68 | """ 69 | Should return default set by settings.LANGUAGE_CODE if no 'lang' 70 | url parameter and no Accept-Language header 71 | """ 72 | request = self.factory.get('/') 73 | self.assertFalse('lang' in request.GET) 74 | self.assertFalse(request.META.get('HTTP_ACCEPT_LANGUAGE')) 75 | prefixer = Prefixer(request) 76 | eq_(prefixer.get_language(), 'en-US') 77 | 78 | @override_settings(LANGUAGE_URL_MAP={'en-us': 'en-US', 'de': 'de'}) 79 | def test_get_language_valid_lang_param(self): 80 | """ 81 | Should return lang param value if it is in settings.LANGUAGE_URL_MAP 82 | """ 83 | request = self.factory.get('/?lang=de') 84 | eq_(request.GET.get('lang'), 'de') 85 | ok_('de' in settings.LANGUAGE_URL_MAP) 86 | prefixer = Prefixer(request) 87 | eq_(prefixer.get_language(), 'de') 88 | 89 | @override_settings(LANGUAGE_CODE='en-US', 90 | LANGUAGE_URL_MAP={'en-us': 'en-US'}) 91 | def test_get_language_invalid_lang_param(self): 92 | """ 93 | Should return default set by settings.LANGUAGE_CODE if lang 94 | param value is not in settings.LANGUAGE_URL_MAP 95 | """ 96 | request = self.factory.get('/?lang=de') 97 | ok_('lang' in request.GET) 98 | self.assertFalse('de' in settings.LANGUAGE_URL_MAP) 99 | prefixer = Prefixer(request) 100 | eq_(prefixer.get_language(), 'en-US') 101 | 102 | def test_get_language_returns_best(self): 103 | """ 104 | Should pass Accept-Language header value to get_best_language 105 | and return result 106 | """ 107 | request = self.factory.get('/') 108 | request.META['HTTP_ACCEPT_LANGUAGE'] = 'de, es' 109 | prefixer = Prefixer(request) 110 | prefixer.get_best_language = Mock(return_value='de') 111 | eq_(prefixer.get_language(), 'de') 112 | prefixer.get_best_language.assert_called_once_with('de, es') 113 | 114 | @override_settings(LANGUAGE_CODE='en-US') 115 | def test_get_language_no_best(self): 116 | """ 117 | Should return default set by settings.LANGUAGE_CODE if 118 | get_best_language return value is None 119 | """ 120 | request = self.factory.get('/') 121 | request.META['HTTP_ACCEPT_LANGUAGE'] = 'de, es' 122 | prefixer = Prefixer(request) 123 | prefixer.get_best_language = Mock(return_value=None) 124 | eq_(prefixer.get_language(), 'en-US') 125 | prefixer.get_best_language.assert_called_once_with('de, es') 126 | 127 | @override_settings(LANGUAGE_URL_MAP={'en-us': 'en-US', 'de': 'de'}) 128 | def test_get_best_language_exact_match(self): 129 | """ 130 | Should return exact match if it is in settings.LANGUAGE_URL_MAP 131 | """ 132 | request = self.factory.get('/') 133 | prefixer = Prefixer(request) 134 | eq_(prefixer.get_best_language('de, es'), 'de') 135 | 136 | @override_settings(LANGUAGE_URL_MAP={'en-us': 'en-US', 'es-ar': 'es-AR'}, 137 | CANONICAL_LOCALES={'es': 'es-ES', 'en': 'en-US'}) 138 | def test_get_best_language_prefix_match(self): 139 | """ 140 | Should return a language with a matching prefix from 141 | settings.LANGUAGE_URL_MAP + settings.CANONICAL_LOCALES if it exists but 142 | no exact match does 143 | """ 144 | request = self.factory.get('/') 145 | prefixer = Prefixer(request) 146 | eq_(prefixer.get_best_language('en'), 'en-US') 147 | eq_(prefixer.get_best_language('en-CA'), 'en-US') 148 | eq_(prefixer.get_best_language('en-GB'), 'en-US') 149 | eq_(prefixer.get_best_language('en-US'), 'en-US') 150 | eq_(prefixer.get_best_language('es'), 'es-ES') 151 | eq_(prefixer.get_best_language('es-AR'), 'es-AR') 152 | eq_(prefixer.get_best_language('es-CL'), 'es-ES') 153 | eq_(prefixer.get_best_language('es-MX'), 'es-ES') 154 | 155 | @override_settings(LANGUAGE_URL_MAP={'en-us': 'en-US'}) 156 | def test_get_best_language_no_match(self): 157 | """ 158 | Should return None if there is no exact match or matching 159 | prefix 160 | """ 161 | request = self.factory.get('/') 162 | prefixer = Prefixer(request) 163 | eq_(prefixer.get_best_language('de'), None) 164 | 165 | @override_settings(LANGUAGE_URL_MAP={'en-us': 'en-US'}) 166 | def test_get_best_language_handles_parse_accept_lang_header_error(self): 167 | """ 168 | Should return None despite error raised by bug described in 169 | https://code.djangoproject.com/ticket/21078 170 | """ 171 | request = self.factory.get('/') 172 | prefixer = Prefixer(request) 173 | eq_(prefixer.get_best_language('en; q=1,'), None) 174 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py26 3 | 4 | [testenv] 5 | deps= 6 | -r{toxinidir}/funfactory/requirements/compiled.txt 7 | -r{toxinidir}/funfactory/requirements/dev.txt 8 | commands= 9 | {envpython} {toxinidir}/tests/run_tests.py [] 10 | 11 | [flake8] 12 | ignore=E121,E123,E124,E126,E127,E128 13 | max-line-length=150 14 | --------------------------------------------------------------------------------