├── .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 |
--------------------------------------------------------------------------------