├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── herokuapp ├── __init__.py ├── bin │ ├── __init__.py │ └── herokuapp_startproject.py ├── commands.py ├── env.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── base.py │ │ └── heroku_audit.py ├── middleware.py ├── project_template │ ├── .gitignore │ ├── .slugignore │ ├── manage.py │ └── project_name │ │ ├── __init__.py │ │ ├── apps │ │ └── __init__.py │ │ ├── settings │ │ ├── .gitignore │ │ ├── __init__.py │ │ ├── local.py │ │ └── production.py │ │ ├── static │ │ └── js │ │ │ └── .gitignore │ │ ├── templates │ │ └── .gitignore │ │ ├── urls.py │ │ └── wsgi.py └── settings.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | .pydevproject 4 | .settings 5 | *.pyc 6 | *.pyo 7 | .bzr 8 | .bzrignore 9 | Thumbs.db 10 | dist 11 | build 12 | MANIFEST 13 | /venv 14 | /*.egg-info/ 15 | /*.egg 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | django-herokuapp changelog 2 | ========================== 3 | 4 | 5 | 0.9.20 - 05/05/2014 6 | ------------------- 7 | 8 | - Fixing error if heroku config command returns no config vars. (@ducheneaut) 9 | - Removed heroku deploy command, since it's now broken with the removal of Anvil. 10 | 11 | 12 | 0.9.19 - 05/12/2014 13 | ------------------- 14 | 15 | - Fixing error if heroku config command returns no config vars. (@brad) 16 | - Removing dependency on South. 17 | 18 | 19 | 0.9.18 - 21/01/2014 20 | ------------------- 21 | 22 | - Fixing issues with executable permissions on scripts installed via herokuapp_startproject. 23 | 24 | 25 | 0.9.17 - 19/01/2014 26 | ------------------- 27 | 28 | - An improved Heroku toolbelt installation should work on all Linux distros (not just on Ubuntu). 29 | - Django 1.5 compatibility. 30 | - Ability to specify a Heroku buildpack using the ``HEROKU_BUILDPACK_URL`` setting. 31 | - Faster deploys by bypassing Dyno scaling when possible. 32 | 33 | 34 | 0.9.16 - 14/01/2014 35 | ------------------- 36 | 37 | - Intelligently determining if syncdb or migrate has to be run during a deploy, and not running if not needed. 38 | 39 | 40 | 0.9.15 - 13/01/2014 41 | ------------------- 42 | 43 | - Unwrapping Django management commands output and error streams for correct unicode handling. 44 | 45 | 46 | 0.9.14 - 10/01/2014 47 | ------------------- 48 | 49 | - Better detection and fallback for scripts that require the Heroku toolbelt, but cannot locate it. 50 | - Default deploy script is aware of popular CI build systems, and only builds on the master branch by default. 51 | - Information about forcing SSL on an app. 52 | - Improving heroku_audit command with detection for more potential problems. 53 | - Allowing email providers other than SendGrid to be used without breaking heroku_audit. 54 | 55 | 56 | 0.9.13 - 03/01/2014 57 | ------------------- 58 | 59 | - Correctly interpreting Django BASE_DIR setting. 60 | 61 | 62 | 0.9.12 - 03/01/2014 63 | ------------------- 64 | 65 | - Adding in Heroku-compatible logging configuration to project template settings. 66 | - Updated project template to match Django 1.6 default settings. 67 | - Added heroku_audit command. 68 | - Removing herokuapp subcommand. Loading the config in manage.py is preferred. 69 | 70 | 71 | 0.9.11 - 20/02/2013 72 | ------------------- 73 | 74 | - Adding an --app parameter to the herokuapp and heroku_deploy management commands. 75 | - Updating Django version for new security release. 76 | - Package updates for all base requirements. 77 | - Changing default HTTP server to waitress. 78 | - Adding default settings for HTTPS proxy headers and allowed hosts. 79 | 80 | 81 | 0.9.9 - 15/01/2012 82 | ------------------ 83 | 84 | - Fixing hash generation for gzipped content. 85 | 86 | 87 | 0.9.8 - 14/01/2012 88 | ------------------ 89 | 90 | - Updating packages, notably django-storages. 91 | - Removing hacked S3 storage, now that django-storages has fixed gzipping. 92 | 93 | 94 | 0.9.7 - 09/01/2012 95 | ------------------ 96 | 97 | - Adding in ``herokuapp`` subcommand and ``heroku_deploy`` command. 98 | - Adding in ``CanonicalDomainMiddleware``. 99 | 100 | 101 | 0.9.6 - 27/12/2012 102 | ------------------ 103 | 104 | - Bugfix for gzipped Amazon S3 static files. 105 | 106 | 107 | 0.9.5 - 26/12/2012 108 | ------------------ 109 | 110 | - Adding support for gzipped Amazon S3 static files. 111 | 112 | 113 | 0.9.4 - 19/12/2012 114 | ------------------ 115 | 116 | - Updating requirements.txt for latest versions of 3rd party packages. 117 | 118 | 119 | 0.9.3 - 28/11/2012 120 | ------------------ 121 | 122 | - Updating requirements.txt for latest versions of 3rd party packages. 123 | 124 | 125 | 0.9.2 - 09/11/2012 126 | ------------------ 127 | 128 | - Updating requirements.txt for latest versions of 3rd party packages. 129 | 130 | 131 | 0.9.1 - 02/11/2012 132 | ------------------ 133 | 134 | - Fixing missing entry in default requirements.txt. 135 | - Fixing typos in README.md. 136 | 137 | 138 | 0.9.0 - 02/11/2012 139 | ------------------ 140 | 141 | - First beta release of django-herokuapp. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, David Hall. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of David Hall nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include .gitignore 4 | recursive-include herokuapp .gitignore .slugignore *.py *.sh 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-herokuapp 2 | ================ 3 | 4 | **django-herokuapp** is a set of utilities and a project template for running 5 | Django sites on `Heroku `_. 6 | 7 | 8 | Why not just use Heroku's Django guide? 9 | --------------------------------------- 10 | 11 | Heroku provides `a guide `_ 12 | that describes a reasonable Django project setup. The django-herokuapp project suggests a more advanced approach 13 | with the following benefits: 14 | 15 | - `waitress `_ is used as an app server instead of 16 | `gunicorn `_. Gunicorn is not recommended for use as a public-facing server, 17 | whereas waitress is hardened for production use. 18 | - Amazon S3 is used to serve static files instead of `django-static `_. 19 | Django `does not recommend `_ 20 | serving static files using a Python app server. 21 | - Various minor security and logging improvements. 22 | 23 | 24 | Starting from scratch 25 | --------------------- 26 | 27 | If you're creating a new Django site for hosting on Heroku, then you can give youself a headstart by running 28 | the ``herokuapp_startproject.py`` script that's bundled with this package from within a fresh virtual environment. 29 | 30 | :: 31 | 32 | $ mkdir your/project/location 33 | $ cd your/project/location 34 | $ git init 35 | $ virtualenv venv 36 | $ source venv/bin/activate 37 | $ pip install django-herokuapp 38 | $ herokuapp_startproject.py 39 | 40 | 41 | The rest of this guide describes the process of adapting an existing Django site to run on Heroku. Even if 42 | you've started from scratch using ``herokuapp_startproject.py``, it's still worth reading, as it will 43 | give you a better understanding of the way your site has been configured. 44 | 45 | 46 | Installing in an existing project 47 | --------------------------------- 48 | 49 | 1. Install django-herokuapp using pip ``pip install django-herokuapp``. 50 | 2. Add ``'herokuapp'`` to your ``INSTALLED_APPS`` setting. 51 | 3. Read the rest of this README for pointers on setting up your Heroku site. 52 | 53 | 54 | Site hosting - waitress 55 | ^^^^^^^^^^^^^^^^^^^^^^^ 56 | 57 | A site hosted on Heroku has to handle traffic without the benefit of a buffering reverse proxy like nginx, which means 58 | that the normal approach of using a small pool of worker threads won't scale in production, particularly if 59 | serving clients over a slow connection. 60 | 61 | The solution is to use a buffering async master thread with sync workers instead, and the 62 | `waitress `_ project provides an excellent implementation of this approach. 63 | 64 | Simply create a file called ``Procfile`` in the root of your project, and add the following line to it: 65 | 66 | :: 67 | 68 | web: waitress-serve --port=$PORT .wsgi:application 69 | 70 | 71 | Database hosting - Heroku Postgres 72 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 73 | 74 | Heroku provides an excellent `Postgres Add-on `_ that you can use for your site. 75 | The recommended settings for using Heroku Postgres are as follows: 76 | 77 | :: 78 | 79 | import dj_database_url 80 | DATABASES = { 81 | "default": dj_database_url.config(default='postgres://localhost'), 82 | } 83 | 84 | This configuration relies on the `dj-database-url `_ package, which 85 | is included in the dependencies for django-herokuapp. 86 | 87 | You can provision a starter package with Heroku Postgres using the following Heroku command: 88 | 89 | :: 90 | 91 | $ heroku addons:create heroku-postgresql:dev 92 | 93 | 94 | Static file hosting - Amazon S3 95 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 96 | 97 | A pure-python webserver like waitress isn't best suited to serving high volumes of static files. For this, a cloud-based 98 | service like `Amazon S3 `_ is ideal. 99 | 100 | The recommended settings for hosting your static content with Amazon S3 is as follows: 101 | 102 | :: 103 | 104 | # Use Amazon S3 for storage for uploaded media files. 105 | DEFAULT_FILE_STORAGE = "storages.backends.s3boto.S3BotoStorage" 106 | 107 | # Use Amazon S3 for static files storage. 108 | STATICFILES_STORAGE = "require_s3.storage.OptimizedCachedStaticFilesStorage" 109 | 110 | # Amazon S3 settings. 111 | AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "") 112 | AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "") 113 | AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME", "") 114 | AWS_AUTO_CREATE_BUCKET = True 115 | AWS_HEADERS = { 116 | "Cache-Control": "public, max-age=86400", 117 | } 118 | AWS_S3_FILE_OVERWRITE = False 119 | AWS_QUERYSTRING_AUTH = False 120 | AWS_S3_SECURE_URLS = True 121 | AWS_REDUCED_REDUNDANCY = False 122 | AWS_IS_GZIPPED = False 123 | 124 | # Cache settings. 125 | CACHES = { 126 | # Long cache timeout for staticfiles, since this is used heavily by the optimizing storage. 127 | "staticfiles": { 128 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 129 | "TIMEOUT": 60 * 60 * 24 * 365, 130 | "LOCATION": "staticfiles", 131 | }, 132 | } 133 | 134 | You can set your AWS account details by running the following command: 135 | 136 | :: 137 | 138 | $ heroku config:set AWS_ACCESS_KEY_ID=your_key_id \ 139 | AWS_SECRET_ACCESS_KEY=your_secret_access_key \ 140 | AWS_STORAGE_BUCKET_NAME=your_bucket_name 141 | 142 | This configuration relies on the `django-require-s3 `_ package, which 143 | is included in the dependencies for django-herokuapp. In particular, the use of `django-require `_ 144 | to compress and serve your assets is recommended, since it allows assets to be precompiled during the project's 145 | build step, rather than on-the-fly as the site is running. 146 | 147 | 148 | Email hosting - SendGrid 149 | ^^^^^^^^^^^^^^^^^^^^^^^^ 150 | 151 | Heroku does not provide an SMTP server in its default package. Instead, it's recommended that you use 152 | the `SendGrid Add-on `_ to send your site's emails. 153 | 154 | :: 155 | 156 | # Email settings. 157 | EMAIL_HOST = "smtp.sendgrid.net" 158 | EMAIL_HOST_USER = os.environ.get("SENDGRID_USERNAME", "") 159 | EMAIL_HOST_PASSWORD = os.environ.get("SENDGRID_PASSWORD", "") 160 | EMAIL_PORT = 25 161 | EMAIL_USE_TLS = False 162 | 163 | You can provision a starter package with SendGrid using the following Heroku command: 164 | 165 | :: 166 | 167 | $ heroku addons:create sendgrid:starter 168 | 169 | 170 | Optimizing compiled slug size 171 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 172 | 173 | The smaller the size of your compiled project, the faster it can be redeployed on Heroku servers. To this end, 174 | django-herokuapp provides a suggested `.slugignore `_ 175 | file that should be placed in the root of your project. If you've used the ``herokuapp_startproject.py`` script 176 | to set up your project, then this will have already been taken care of for you. 177 | 178 | 179 | Improving site security 180 | ^^^^^^^^^^^^^^^^^^^^^^^ 181 | 182 | Ideally, you should not store your site's ``SECRET_KEY`` setting in version control. Instead, it should be read 183 | from the Heroku config as follows: 184 | 185 | :: 186 | 187 | from django.utils.crypto import get_random_string 188 | SECRET_KEY = os.environ.get("SECRET_KEY", get_random_string(50, "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)")) 189 | 190 | You can then generate a secret key in your Heroku config with the following command: 191 | 192 | :: 193 | 194 | $ heroku config:set SECRET_KEY=`openssl rand -base64 32` 195 | 196 | It's also recommended that you configure Python to generate a new random seed every time it boots. 197 | 198 | :: 199 | 200 | $ heroku config:set PYTHONHASHSEED=random 201 | 202 | 203 | Adding support for Heroku SSL 204 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 205 | 206 | Heroku provides a free `piggyback SSL `_ 207 | service for all of its apps, as well as a `SSL endpoint addon `_ 208 | for custom domains. It order to detect when a request is made via SSL in Django (for use in `request.is_secure()`), 209 | you should add the following setting to your app: 210 | 211 | :: 212 | 213 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 214 | 215 | If you intend to serve your entire app over SSL, then it's a good idea to force all requests to use SSL. The 216 | `django-sslify `_ app provides a middleware for this. Simply `pip install django-sslify`, 217 | then add ``"sslify.middleware.SSLifyMiddleware"`` to the start of your ``MIDDLEWARE_CLASSES``. 218 | 219 | 220 | Outputting logs to Heroku logplex 221 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 222 | 223 | By default, Django does not log errors to STDERR in production, which is the correct behaviour for most WSGI 224 | apps. Heroku, however, provides an excellent logging service that expects to receive error messages on STDERR. 225 | You can take advantage of this by updating your logging configuration to the following: 226 | 227 | :: 228 | 229 | LOGGING = { 230 | "version": 1, 231 | "disable_existing_loggers": False, 232 | "handlers": { 233 | "console": { 234 | "level": "INFO", 235 | "class": "logging.StreamHandler", 236 | }, 237 | }, 238 | "loggers": { 239 | "django": { 240 | "handlers": ["console"], 241 | } 242 | } 243 | } 244 | 245 | 246 | Running your site in the Heroku environment 247 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 248 | 249 | Because your site is setup to read some of its configuration from environmental variables stored on 250 | Heroku, running a development server can be tricky. django-herokuapp provides a configuration utility 251 | that should be added to your project to load the heroku config dynamically. Simply add 252 | the following lines to your ``manage.py`` script, at the top of the run block: 253 | 254 | :: 255 | 256 | if __name__ == "__main__": # << This line will already be present in manage.py 257 | 258 | # Load the Heroku environment. 259 | from herokuapp.env import load_env 260 | load_env(__file__, "your-app-name") 261 | 262 | Django management commands can then be run normally: 263 | 264 | :: 265 | 266 | $ python manage.py runserver 267 | 268 | Accessing the live Heroku Postgres database is a bad idea. Instead, you should provide a local settings file, 269 | exclude it from version control, and connect to a local PostgreSQL server. If you're 270 | on OSX, then the excellent `Postgres.app `_ will make this very easy. 271 | 272 | A suggested settings file layout, including the appropriate local settings, can be found in the `django-herokuapp 273 | template project settings directory `_. 274 | 275 | 276 | Validating your Heroku setup 277 | ---------------------------- 278 | 279 | Once you've completed the above steps, and are confident that your site is suitable to deploy to Heroku, 280 | you can validate against common errors by running the ``heroku_audit`` management command. 281 | 282 | :: 283 | 284 | $ python manage.py heroku_audit 285 | 286 | Many of the issues detected by ``heroku_audit`` have simple fixes. For a guided walkthrough of solutions, try 287 | running: 288 | 289 | :: 290 | 291 | $ python manage.py heroku_audit --fix 292 | 293 | 294 | Common error messages 295 | --------------------- 296 | 297 | Things don't always go right first time. Here are some common error messages you may encounter: 298 | 299 | 300 | "No app specified" when running Heroku commands 301 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 302 | 303 | The Heroku CLI looks up your app's name from a git remote named ``heroku``. You can either specify the app 304 | to manage by adding ``-a your-app-name`` every time you call a Heroku command, or update your git repo with a 305 | Heroku remote using the following command: 306 | 307 | :: 308 | 309 | $ git remote add heroku git@heroku.com:your-app-name.git 310 | 311 | 312 | "AttributeError: 'Settings' object has no attribute 'BASE_DIR'" 313 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 314 | 315 | Many django-herokuapp commands need to know the root of the project's file stucture. Django 1.6 provides 316 | this setting automatically as ``settings.BASE_DIR``. If this setting is not present in your settings file, 317 | it should be added as an absolute path. You can look it up dynamically from the settings file like this: 318 | 319 | :: 320 | 321 | import os.path 322 | # Assumes the settings file is located in `your_project.settings` package. 323 | BASE_DIR = os.path.abspath(os.path.join(__file__, "..", "..")) 324 | 325 | 326 | Support and announcements 327 | ------------------------- 328 | 329 | Downloads and bug tracking can be found at the `main project website `_. 330 | 331 | 332 | More information 333 | ---------------- 334 | 335 | The django-herokuapp project was developed by Dave Hall. You can get the code 336 | from the `django-herokuapp project site `_. 337 | 338 | Dave Hall is a freelance web developer, based in Cambridge, UK. You can usually 339 | find him on the Internet in a number of different places: 340 | 341 | - `Website `_ 342 | - `Twitter `_ 343 | - `Google Profile `_ 344 | -------------------------------------------------------------------------------- /herokuapp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of utilities and a project template for running Django sites on heroku. 3 | 4 | Developed by Dave Hall. 5 | 6 | 7 | """ 8 | 9 | 10 | __version__ = (0, 9, 20) -------------------------------------------------------------------------------- /herokuapp/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etianen/django-herokuapp/1d3ec10f6e83b7556a443150aa0658bbf341f6d1/herokuapp/bin/__init__.py -------------------------------------------------------------------------------- /herokuapp/bin/herokuapp_startproject.py: -------------------------------------------------------------------------------- 1 | import sys, os, stat, os.path, getpass, subprocess, argparse 2 | 3 | from django.core import management 4 | 5 | 6 | parser = argparse.ArgumentParser( 7 | description = "Start a new herokuapp Django project.", 8 | ) 9 | parser.add_argument("project_name", 10 | help = "The name of the project to create.", 11 | ) 12 | parser.add_argument("dest_dir", 13 | default = ".", 14 | nargs = "?", 15 | help = "The destination dir for the created project.", 16 | ) 17 | parser.add_argument("-a", "--app", 18 | default = None, 19 | dest = "app", 20 | required = False, 21 | help = "The name of the Heroku app. Defaults to the project name, with underscores replaced by hyphens.", 22 | ) 23 | parser.add_argument("--noinput", 24 | action = "store_false", 25 | default = True, 26 | dest = "interactive", 27 | help = "Tells Django to NOT prompt the user for input of any kind.", 28 | ) 29 | 30 | 31 | def make_executable(path): 32 | st = os.stat(path) 33 | os.chmod(path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) 34 | 35 | 36 | def main(): 37 | args = parser.parse_args() 38 | # Generate Heroku app name. 39 | app_name = args.app or args.project_name.replace("_", "-") 40 | # Create the project. 41 | try: 42 | os.makedirs(args.dest_dir) 43 | except OSError: 44 | pass 45 | management.call_command("startproject", 46 | args.project_name, 47 | args.dest_dir, 48 | template = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "project_template")), 49 | extensions = ("py", "txt", "slugignore", "conf", "gitignore", "sh",), 50 | app_name = app_name, 51 | user = getpass.getuser(), 52 | ) 53 | # Make management scripts executable. 54 | make_executable(os.path.join(args.dest_dir, "manage.py")) 55 | # Audit and configure the project for Heroku. 56 | audit_args = ["python", os.path.join(args.dest_dir, "manage.py"), "heroku_audit", "--fix"] 57 | if not args.interactive: 58 | audit_args.append("--noinput") 59 | audit_returncode = subprocess.call(audit_args) 60 | if audit_returncode != 0: 61 | sys.exit(audit_returncode) 62 | # Give some help to the user. 63 | print "Heroku project created." 64 | -------------------------------------------------------------------------------- /herokuapp/commands.py: -------------------------------------------------------------------------------- 1 | import re, os 2 | from collections import Counter 3 | from functools import partial 4 | 5 | import sh 6 | 7 | from django.core.management import CommandError 8 | from django.utils.encoding import force_text 9 | 10 | 11 | RE_PARSE_SHELL = re.compile("^.*=.*$", re.MULTILINE) 12 | 13 | RE_PS = re.compile("^(\w+)\.") 14 | 15 | RE_POSTGRES = re.compile("^HEROKU_POSTGRESQL_\w+?_URL$") 16 | 17 | 18 | def parse_shell(lines): 19 | """ Parse config variables from the lines """ 20 | 21 | # If there are no config variables, return an empty dict 22 | if not RE_PARSE_SHELL.search(str(lines)): 23 | return dict() 24 | return dict( 25 | line.strip().split("=", 1) 26 | for line 27 | in lines 28 | ) 29 | 30 | 31 | def format_command(prefix, args, kwargs): 32 | return u"COMMAND: {prefix} {args} {kwargs}".format( 33 | prefix = prefix, 34 | args = u" ".join(map(force_text, args)), 35 | kwargs = u" ".join( 36 | u"--{key}={value}".format( 37 | key = key.replace("_", "-"), 38 | value = value, 39 | ) 40 | for key, value 41 | in kwargs.items() 42 | ) 43 | ) 44 | 45 | 46 | class HerokuCommandError(CommandError): 47 | 48 | pass 49 | 50 | 51 | class HerokuCommand(object): 52 | 53 | def __init__(self, app, cwd, stdout=None, stderr=None, dry_run=False): 54 | # Store the dry run state. 55 | self.dry_run = dry_run 56 | self._stdout = stdout 57 | # Check that the heroku command is available. 58 | if hasattr(sh, "heroku"): 59 | heroku_command = sh.heroku 60 | else: 61 | raise HerokuCommandError("Heroku toolbelt is not installed. Install from https://toolbelt.heroku.com/") 62 | # Create the Heroku command wrapper. 63 | self._heroku = partial(heroku_command, 64 | _cwd = cwd, 65 | _out = stdout, 66 | _err = stderr, 67 | ) # Not using bake(), as it gets the command order wrong. 68 | # Ensure that the user is logged in. 69 | def auth_token_interact(line, stdin, process): 70 | if line == "\n": 71 | stdin.put("\n") 72 | try: 73 | self("auth:token", _force_live_run=True, _in=None, _tty_in=True, _out=auth_token_interact, _out_bufsize=0).wait() 74 | except sh.ErrorReturnCode: 75 | raise HerokuCommandError("Please log in to the Heroku Toolbelt using `heroku auth:login`.") 76 | # Apply the app argument. 77 | if app: 78 | self._heroku = partial(self._heroku, app=app) 79 | 80 | def __call__(self, *args, **kwargs): 81 | # Allow dry run to be overridden for selective (non-mutating) commands. 82 | force_live_run = kwargs.pop("_force_live_run", False) 83 | # Run the command. 84 | if self.dry_run and not force_live_run: 85 | # Allow a dry run to be processed. 86 | self._stdout.write(format_command("heroku", args, kwargs) + "\n") 87 | else: 88 | # Call a live command. 89 | try: 90 | return self._heroku(*args, **kwargs) 91 | except sh.ErrorReturnCode as ex: 92 | raise HerokuCommandError(force_text(ex)) 93 | 94 | def config_set(self, **kwargs): 95 | return self("config:set", *[ 96 | "{key}={value}".format( 97 | key = key, 98 | value = value, 99 | ) 100 | for key, value 101 | in kwargs.items() 102 | ], _out=None) 103 | 104 | def config_get(self, name=None): 105 | if name: 106 | return str(self("config:get", name, _out=None, _force_live_run=True)).strip() 107 | return parse_shell(self("config", shell=True, _out=None)) 108 | 109 | def ps(self): 110 | counter = Counter() 111 | for line in self("ps", _out=None, _iter=True, _force_live_run=True): 112 | match = RE_PS.match(line) 113 | if match: 114 | process_name = match.group(1) 115 | if process_name not in ("run"): 116 | counter[process_name] += 1 117 | return counter 118 | 119 | def scale(self, **kwargs): 120 | return self("ps:scale", *[ 121 | "{name}={count}".format( 122 | name = name, 123 | count = count, 124 | ) 125 | for name, count 126 | in kwargs.items() 127 | ]) 128 | 129 | def postgres_url(self): 130 | for line in self("config", shell=True, _out=None, _force_live_run=True): 131 | key = line.split("=", 1)[0] 132 | if RE_POSTGRES.match(key): 133 | return key 134 | -------------------------------------------------------------------------------- /herokuapp/env.py: -------------------------------------------------------------------------------- 1 | import os, os.path 2 | 3 | from herokuapp.commands import HerokuCommand, HerokuCommandError 4 | 5 | 6 | def load_env(entrypoint, app=None): 7 | try: 8 | heroku = HerokuCommand( 9 | app = app, 10 | cwd = os.path.dirname(entrypoint), 11 | ) 12 | heroku_config = heroku.config_get() 13 | except HerokuCommandError: 14 | pass 15 | else: 16 | for key, value in heroku_config.items(): 17 | os.environ.setdefault(key, value) 18 | -------------------------------------------------------------------------------- /herokuapp/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etianen/django-herokuapp/1d3ec10f6e83b7556a443150aa0658bbf341f6d1/herokuapp/management/__init__.py -------------------------------------------------------------------------------- /herokuapp/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etianen/django-herokuapp/1d3ec10f6e83b7556a443150aa0658bbf341f6d1/herokuapp/management/commands/__init__.py -------------------------------------------------------------------------------- /herokuapp/management/commands/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from optparse import make_option 4 | 5 | from django.utils.functional import cached_property 6 | from django.conf import settings 7 | from django.core.management import call_command 8 | 9 | from herokuapp.commands import HerokuCommand, format_command 10 | from herokuapp.settings import HEROKU_APP_NAME 11 | 12 | 13 | class HerokuCommandMixin(object): 14 | 15 | option_list = ( 16 | make_option("-a", "--app", 17 | default = HEROKU_APP_NAME, 18 | dest = "app", 19 | help = "The name of the Heroku app to use. Defaults to HEROKU_APP_NAME.", 20 | ), 21 | make_option("--dry-run", 22 | action = "store_true", 23 | default = False, 24 | dest = "dry_run", 25 | help = "Outputs the heroku and django managment commands that will be run, but doesn't execute them.", 26 | ), 27 | ) 28 | 29 | def call_command(self, *args, **kwargs): 30 | """ 31 | Calls the given management command, but only if it's not a dry run. 32 | 33 | If it's a dry run, then a notice about the command will be printed. 34 | """ 35 | if self.dry_run: 36 | self.stdout.write(format_command("python manage.py", args, kwargs)) 37 | else: 38 | call_command(*args, **kwargs) 39 | 40 | @cached_property 41 | def heroku(self): 42 | return HerokuCommand( 43 | app = self.app, 44 | cwd = settings.BASE_DIR, 45 | stdout = self.stdout._out, 46 | stderr = self.stderr._out, 47 | dry_run = self.dry_run, 48 | ) 49 | -------------------------------------------------------------------------------- /herokuapp/management/commands/heroku_audit.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os, os.path, sys 4 | from optparse import make_option 5 | 6 | import sh 7 | 8 | from django.conf import settings 9 | from django.core.management.base import NoArgsCommand, BaseCommand 10 | from django.utils.crypto import get_random_string 11 | from django.core.files.storage import default_storage 12 | 13 | from storages.backends.s3boto import S3BotoStorage 14 | 15 | from herokuapp.commands import HerokuCommandError 16 | from herokuapp.management.commands.base import HerokuCommandMixin 17 | 18 | 19 | class Command(HerokuCommandMixin, NoArgsCommand): 20 | 21 | help = "Tests this app for common Heroku deployment issues." 22 | 23 | option_list = BaseCommand.option_list + ( 24 | make_option("--noinput", 25 | action = "store_false", 26 | default = True, 27 | dest = "interactive", 28 | help = "Tells Django to NOT prompt the user for input of any kind.", 29 | ), 30 | make_option("--fix", 31 | action = "store_true", 32 | default = False, 33 | dest = "fix", 34 | help = "If specified, then the user will be prompted to fix problems as they are found. Combine with --noinput to auto-fix problems.", 35 | ), 36 | ) + HerokuCommandMixin.option_list 37 | 38 | def exit_with_error(self, error): 39 | self.stderr.write(error) 40 | self.stderr.write("Heroku audit aborted.") 41 | self.stderr.write("Run `python manage.py heroku_audit --fix` to fix problems.") 42 | sys.exit(1) 43 | 44 | def prompt_for_fix(self, error, message): 45 | if self.fix: 46 | self.stdout.write(error) 47 | if self.interactive: 48 | # Ask to fix the issue. 49 | answer = "" 50 | while not answer in ("y", "n"): 51 | answer = raw_input("{message} (y/n) > ".format( 52 | message = message, 53 | )).lower().strip() 54 | answer_bool = answer == "y" 55 | else: 56 | # Attempt to auto-fix the issue. 57 | answer_bool = True 58 | else: 59 | answer_bool = False 60 | # Exit if no fix provided. 61 | if not answer_bool: 62 | self.exit_with_error(error) 63 | 64 | def read_string(self, message, default): 65 | if self.interactive: 66 | answer = "" 67 | while not answer: 68 | answer = raw_input("{message} {default}> ".format( 69 | message = message, 70 | default = "({default}) ".format( 71 | default = default, 72 | ) if default else "" 73 | )).strip() or default 74 | return answer 75 | else: 76 | return default 77 | 78 | def handle(self, **kwargs): 79 | self.app = kwargs["app"] 80 | self.dry_run = kwargs["dry_run"] 81 | self.interactive = kwargs["interactive"] 82 | self.fix = kwargs["fix"] 83 | # Check app exists. 84 | try: 85 | self.heroku("apps:info") 86 | except HerokuCommandError: 87 | self.prompt_for_fix("No Heroku app named '{app}' detected.".format(app=self.app), "Create app?") 88 | self.heroku("apps:create", self.app) 89 | self.stdout.write("Heroku app created.") 90 | # Check that Amazon S3 is being used for media. 91 | default_storage._setup() 92 | if not isinstance(default_storage._wrapped, S3BotoStorage): 93 | self.exit_with_error("settings.DEFAULT_FILE_STORAGE should be set to a subclass of `storages.backends.s3boto.S3BotoStorage`.") 94 | # Check for AWS access details. 95 | if not self.heroku.config_get("AWS_ACCESS_KEY_ID"): 96 | self.prompt_for_fix("Amazon S3 access details not present in Heroku config.", "Setup now?") 97 | aws_env = {} 98 | aws_env["AWS_ACCESS_KEY_ID"] = self.read_string("AWS access key", os.environ.get("AWS_ACCESS_KEY_ID")) 99 | aws_env["AWS_SECRET_ACCESS_KEY"] = self.read_string("AWS access secret", os.environ.get("AWS_SECRET_ACCESS_KEY")) 100 | aws_env["AWS_STORAGE_BUCKET_NAME"] = self.read_string("S3 bucket name", self.app) 101 | # Save Heroku config. 102 | self.heroku.config_set(**aws_env) 103 | self.stdout.write("Amazon S3 config written to Heroku config.") 104 | # Check for SendGrid settings. 105 | if settings.EMAIL_HOST == "smtp.sendgrid.net" and not self.heroku.config_get("SENDGRID_USERNAME"): 106 | self.prompt_for_fix("SendGrid addon not installed.", "Provision SendGrid starter addon (free)?") 107 | self.heroku("addons:add", "sendgrid:starter") 108 | self.stdout.write("SendGrid addon provisioned.") 109 | # Check for promoted database URL. 110 | if not self.heroku.config_get("DATABASE_URL"): 111 | database_url = self.heroku.postgres_url() 112 | if not database_url: 113 | self.prompt_for_fix("Database URL not present in Heroku config.", "Provision Heroku Postgres dev addon (free)?") 114 | self.heroku("addons:add", "heroku-postgresql") 115 | self.heroku("pg:wait") 116 | self.stdout.write("Heroku Postgres addon provisioned.") 117 | # Load the new database URL. 118 | database_url = self.heroku.postgres_url() 119 | # Promote the database URL. 120 | self.prompt_for_fix("No primary database URL set.", "Promote {database_url}?".format(database_url=database_url)) 121 | self.heroku("pg:promote", database_url) 122 | self.stdout.write("Heroku primary database URL set.") 123 | # Check for secret key. 124 | heroku_secret_key = self.heroku.config_get("SECRET_KEY") 125 | if not heroku_secret_key: 126 | self.prompt_for_fix("Secret key not set in Heroku config.", "Generate now?") 127 | self.heroku.config_set(SECRET_KEY=get_random_string(50, "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)")) 128 | self.stdout.write("Secret key written to Heroku config. Ensure your settings file is configured to read the secret key: https://github.com/etianen/django-herokuapp#improving-site-security") 129 | # Check for Python hash seed. 130 | if not self.heroku.config_get("PYTHONHASHSEED"): 131 | self.prompt_for_fix("Python hash seed not set in Heroku config.", "Set now?") 132 | self.heroku.config_set(PYTHONHASHSEED="random") 133 | self.stdout.write("Python hash seed written to Heroku config.") 134 | # Check for SSL header settings. 135 | if not getattr(settings, "SECURE_PROXY_SSL_HEADER", None) == ("HTTP_X_FORWARDED_PROTO", "https"): 136 | self.exit_with_error("Missing SECURE_PROXY_SSL_HEADER settings. Please add `SECURE_PROXY_SSL_HEADER = (\"HTTP_X_FORWARDED_PROTO\", \"https\")` to your settings.py file.") 137 | # Check for Procfile. 138 | procfile_path = os.path.join(settings.BASE_DIR, "Procfile") 139 | if not os.path.exists(procfile_path): 140 | self.prompt_for_fix("Procfile must to be created to deploy to Heroku.", "Create now?") 141 | with open(procfile_path, "wb") as procfile_handle: 142 | procfile_handle.write("web: waitress-serve --port=$PORT {project_name}.wsgi:application\n".format( 143 | project_name = os.environ["DJANGO_SETTINGS_MODULE"].split(".", 1)[0], 144 | )) 145 | self.stdout.write("Default Procfile generated.") 146 | # Check for requirements.txt. 147 | requirements_path = os.path.join(settings.BASE_DIR, "requirements.txt") 148 | if not os.path.exists(requirements_path): 149 | self.prompt_for_fix("A requirements.txt file must be created to deploy to Heroku.", "Generate now?") 150 | sh.pip.freeze(_out=requirements_path) 151 | self.stdout.write("Dependencies frozen to requirements.txt.") 152 | -------------------------------------------------------------------------------- /herokuapp/middleware.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | from django.core.exceptions import MiddlewareNotUsed 3 | from django.conf import settings 4 | 5 | from herokuapp.settings import SITE_DOMAIN 6 | 7 | 8 | class CanonicalDomainMiddleware(object): 9 | 10 | """Middleware that redirects to a canonical domain.""" 11 | 12 | def __init__(self): 13 | if settings.DEBUG or not SITE_DOMAIN: 14 | raise MiddlewareNotUsed 15 | 16 | def process_request(self, request): 17 | """If the request domain is not the canonical domain, redirect.""" 18 | hostname = request.get_host().split(":", 1)[0] 19 | # Don't perform redirection for testing or local development. 20 | if hostname in ("testserver", "localhost", "127.0.0.1"): 21 | return 22 | # Check against the site domain. 23 | canonical_hostname = SITE_DOMAIN.split(":", 1)[0] 24 | if hostname != canonical_hostname: 25 | if request.is_secure(): 26 | canonical_url = "https://" 27 | else: 28 | canonical_url = "http://" 29 | canonical_url += SITE_DOMAIN + request.get_full_path() 30 | return redirect(canonical_url, permanent=True) -------------------------------------------------------------------------------- /herokuapp/project_template/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | .pydevproject 4 | .settings 5 | *.pyc 6 | *.pyo 7 | .bzr 8 | .bzrignore 9 | Thumbs.db 10 | /dist 11 | /build 12 | /MANIFEST 13 | /venv 14 | /.env 15 | /static 16 | /media 17 | -------------------------------------------------------------------------------- /herokuapp/project_template/.slugignore: -------------------------------------------------------------------------------- 1 | tests 2 | tests.py 3 | tests.pyc 4 | static 5 | local.py 6 | local.pyc 7 | node_modules 8 | -------------------------------------------------------------------------------- /herokuapp/project_template/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | 7 | # Load the Heroku environment. 8 | from herokuapp.env import load_env 9 | load_env(__file__, "{{ app_name }}") 10 | 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") 12 | 13 | from django.core.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etianen/django-herokuapp/1d3ec10f6e83b7556a443150aa0658bbf341f6d1/herokuapp/project_template/project_name/__init__.py -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etianen/django-herokuapp/1d3ec10f6e83b7556a443150aa0658bbf341f6d1/herokuapp/project_template/project_name/apps/__init__.py -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/settings/.gitignore: -------------------------------------------------------------------------------- 1 | /local.py 2 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/settings/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings used by {{ project_name }} project. 3 | 4 | This consists of the general production settings, with an optional import of any local 5 | settings. 6 | """ 7 | 8 | # Import production settings. 9 | from {{ project_name }}.settings.production import * 10 | 11 | # Import optional local settings. 12 | try: 13 | from {{ project_name }}.settings.local import * 14 | except ImportError: 15 | pass 16 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/settings/local.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for local development. 3 | 4 | These settings are not fast or efficient, but allow local servers to be run 5 | using the django-admin.py utility. 6 | 7 | This file should be excluded from version control to keep the settings local. 8 | """ 9 | 10 | import os.path 11 | 12 | from production import BASE_DIR 13 | 14 | 15 | # Run in debug mode. 16 | 17 | DEBUG = True 18 | 19 | TEMPLATE_DEBUG = DEBUG 20 | 21 | 22 | # Serve staticfiles locally for development. 23 | 24 | STATICFILES_STORAGE = "require.storage.OptimizedCachedStaticFilesStorage" 25 | 26 | STATIC_URL = "/static/" 27 | 28 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 29 | 30 | 31 | # Use local server. 32 | 33 | SITE_DOMAIN = "localhost:8000" 34 | 35 | PREPEND_WWW = False 36 | 37 | ALLOWED_HOSTS = ("*",) 38 | 39 | 40 | # Disable the template cache for development. 41 | 42 | TEMPLATE_LOADERS = ( 43 | "django.template.loaders.filesystem.Loader", 44 | "django.template.loaders.app_directories.Loader", 45 | ) 46 | 47 | 48 | # Local database settings. These should work well with http://postgresapp.com/. 49 | 50 | DATABASES = { 51 | "default": { 52 | "ENGINE": "django.db.backends.postgresql_psycopg2", 53 | "HOST": "localhost", 54 | "NAME": "{{ project_name }}", 55 | "USER": "{{ user }}", 56 | "PASSWORD": "", 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/settings/production.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for bar project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | import os 12 | import dj_database_url 13 | from django.utils.crypto import get_random_string 14 | 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | 18 | SITE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 19 | 20 | BASE_DIR = os.path.abspath(os.path.join(SITE_ROOT, "..")) 21 | 22 | 23 | # Heroku platform settings. 24 | 25 | HEROKU_APP_NAME = "{{ app_name }}" 26 | 27 | HEROKU_BUILDPACK_URL = "https://github.com/heroku/heroku-buildpack-python.git" 28 | 29 | 30 | # The name and domain of this site. 31 | 32 | SITE_NAME = "Example" 33 | 34 | SITE_DOMAIN = "{{ app_name }}.herokuapp.com" 35 | 36 | PREPEND_WWW = False 37 | 38 | 39 | # Security settings. 40 | 41 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 42 | 43 | ALLOWED_HOSTS = ( 44 | SITE_DOMAIN, 45 | "{HEROKU_APP_NAME}.herokuapp.com".format( 46 | HEROKU_APP_NAME = HEROKU_APP_NAME, 47 | ), 48 | ) 49 | 50 | 51 | # Database settings. 52 | 53 | DATABASES = { 54 | "default": dj_database_url.config(default="postgresql://"), 55 | } 56 | 57 | 58 | # Use Amazon S3 for storage for uploaded media files. 59 | 60 | DEFAULT_FILE_STORAGE = "storages.backends.s3boto.S3BotoStorage" 61 | 62 | 63 | # Use Amazon S3 and RequireJS for static files storage. 64 | 65 | STATICFILES_STORAGE = "require_s3.storage.OptimizedCachedStaticFilesStorage" 66 | 67 | 68 | # Amazon S3 settings. 69 | 70 | AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") 71 | 72 | AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") 73 | 74 | AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") 75 | 76 | AWS_AUTO_CREATE_BUCKET = True 77 | 78 | AWS_HEADERS = { 79 | "Cache-Control": "public, max-age=86400", 80 | } 81 | 82 | AWS_S3_FILE_OVERWRITE = False 83 | 84 | AWS_QUERYSTRING_AUTH = False 85 | 86 | AWS_S3_SECURE_URLS = True 87 | 88 | AWS_REDUCED_REDUNDANCY = False 89 | 90 | AWS_IS_GZIPPED = False 91 | 92 | STATIC_URL = "https://{bucket_name}.s3.amazonaws.com/".format( 93 | bucket_name = AWS_STORAGE_BUCKET_NAME, 94 | ) 95 | 96 | 97 | # Email settings. 98 | 99 | EMAIL_HOST = "smtp.sendgrid.net" 100 | 101 | EMAIL_HOST_USER = os.environ.get("SENDGRID_USERNAME") 102 | 103 | EMAIL_HOST_PASSWORD = os.environ.get("SENDGRID_PASSWORD") 104 | 105 | EMAIL_PORT = 25 106 | 107 | EMAIL_USE_TLS = False 108 | 109 | SERVER_EMAIL = u"{name} ".format( 110 | name = SITE_NAME, 111 | domain = SITE_DOMAIN, 112 | ) 113 | 114 | DEFAULT_FROM_EMAIL = SERVER_EMAIL 115 | 116 | EMAIL_SUBJECT_PREFIX = "[%s] " % SITE_NAME 117 | 118 | 119 | # Error reporting settings. Use these to set up automatic error notifications. 120 | 121 | ADMINS = () 122 | 123 | MANAGERS = () 124 | 125 | SEND_BROKEN_LINK_EMAILS = False 126 | 127 | 128 | # Locale settings. 129 | 130 | TIME_ZONE = "UTC" 131 | 132 | LANGUAGE_CODE = "en-gb" 133 | 134 | USE_I18N = True 135 | 136 | USE_L10N = True 137 | 138 | USE_TZ = True 139 | 140 | 141 | # A list of additional installed applications. 142 | 143 | INSTALLED_APPS = ( 144 | "django.contrib.sessions", 145 | "django.contrib.auth", 146 | "django.contrib.contenttypes", 147 | "django.contrib.messages", 148 | "django.contrib.staticfiles", 149 | "django.contrib.admin", 150 | "herokuapp", 151 | ) 152 | 153 | 154 | # Additional static file locations. 155 | 156 | STATICFILES_DIRS = ( 157 | os.path.join(SITE_ROOT, "static"), 158 | ) 159 | 160 | 161 | # Dispatch settings. 162 | 163 | MIDDLEWARE_CLASSES = ( 164 | "django.middleware.gzip.GZipMiddleware", 165 | "herokuapp.middleware.CanonicalDomainMiddleware", 166 | "django.contrib.sessions.middleware.SessionMiddleware", 167 | "django.middleware.common.CommonMiddleware", 168 | "django.middleware.csrf.CsrfViewMiddleware", 169 | "django.contrib.auth.middleware.AuthenticationMiddleware", 170 | "django.contrib.messages.middleware.MessageMiddleware", 171 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 172 | ) 173 | 174 | ROOT_URLCONF = "{{ project_name }}.urls" 175 | 176 | WSGI_APPLICATION = "{{ project_name }}.wsgi.application" 177 | 178 | SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" 179 | 180 | MESSAGE_STORAGE = "django.contrib.messages.storage.cookie.CookieStorage" 181 | 182 | SITE_ID = 1 183 | 184 | 185 | # Absolute path to the directory where templates are stored. 186 | 187 | TEMPLATE_DIRS = ( 188 | os.path.join(SITE_ROOT, "templates"), 189 | ) 190 | 191 | TEMPLATE_LOADERS = ( 192 | ("django.template.loaders.cached.Loader", ( 193 | "django.template.loaders.filesystem.Loader", 194 | "django.template.loaders.app_directories.Loader", 195 | )), 196 | ) 197 | 198 | TEMPLATE_CONTEXT_PROCESSORS = ( 199 | "django.contrib.auth.context_processors.auth", 200 | "django.core.context_processors.debug", 201 | "django.core.context_processors.i18n", 202 | "django.core.context_processors.media", 203 | "django.core.context_processors.static", 204 | "django.core.context_processors.tz", 205 | # "django.core.context_processors.request", 206 | "django.contrib.messages.context_processors.messages", 207 | ) 208 | 209 | 210 | # Namespace for cache keys, if using a process-shared cache. 211 | 212 | CACHE_MIDDLEWARE_KEY_PREFIX = "{{ project_name }}" 213 | 214 | CACHES = { 215 | "default": { 216 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 217 | }, 218 | # Long cache timeout for staticfiles, since this is used heavily by the optimizing storage. 219 | "staticfiles": { 220 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 221 | "TIMEOUT": 60 * 60 * 24 * 365, 222 | "LOCATION": "staticfiles", 223 | }, 224 | } 225 | 226 | 227 | # A secret key used for cryptographic algorithms. 228 | 229 | SECRET_KEY = os.environ.get("SECRET_KEY", get_random_string(50, "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)")) 230 | 231 | 232 | # Logging configuration. 233 | 234 | LOGGING = { 235 | "version": 1, 236 | # Don't throw away default loggers. 237 | "disable_existing_loggers": False, 238 | "handlers": { 239 | # Redefine console logger to run in production. 240 | "console": { 241 | "level": "INFO", 242 | "class": "logging.StreamHandler", 243 | }, 244 | }, 245 | "loggers": { 246 | # Redefine django logger to use redefined console logging. 247 | "django": { 248 | "handlers": ["console"], 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/static/js/.gitignore: -------------------------------------------------------------------------------- 1 | # Preserves folder structure. -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Preserves folder structure. -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | from django.contrib import admin 3 | from django.views import generic 4 | 5 | 6 | admin.autodiscover() 7 | 8 | 9 | urlpatterns = patterns("", 10 | 11 | # Admin URLs. 12 | url(r"^admin/", include(admin.site.urls)), 13 | 14 | # There's no favicon here! 15 | url(r"^favicon.ico$", generic.RedirectView.as_view()), 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /herokuapp/project_template/project_name/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{ project_name }} project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /herokuapp/settings.py: -------------------------------------------------------------------------------- 1 | """Settings used by django-herokuapp.""" 2 | 3 | from django.conf import settings 4 | 5 | 6 | # The name of the app on the Heroku platform. 7 | HEROKU_APP_NAME = getattr(settings, "HEROKU_APP_NAME", None) 8 | 9 | # The optional explicit buildpack URL. 10 | HEROKU_BUILDPACK_URL = getattr(settings, "HEROKU_BUILDPACK_URL", None) 11 | 12 | # The canonical site domain. 13 | SITE_DOMAIN = getattr(settings, "SITE_DOMAIN", None) 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from herokuapp import __version__ 4 | 5 | 6 | version_str = ".".join(str(n) for n in __version__) 7 | 8 | 9 | setup( 10 | name = "django-herokuapp", 11 | version = version_str, 12 | license = "BSD", 13 | description = "A set of utilities and a project template for running Django sites on heroku.", 14 | author = "Dave Hall", 15 | author_email = "dave@etianen.com", 16 | url = "https://github.com/etianen/django-herokuapp", 17 | entry_points = { 18 | "console_scripts": [ 19 | "herokuapp_startproject.py = herokuapp.bin.herokuapp_startproject:main", 20 | ], 21 | }, 22 | packages = find_packages(), 23 | include_package_data = True, 24 | test_suite = "herokuapp.tests", 25 | install_requires = [ 26 | "django>=1.7", 27 | "pytz", 28 | "waitress", 29 | "dj-database-url", 30 | "psycopg2", 31 | "django-require-s3", 32 | "boto", 33 | "sh", 34 | ], 35 | classifiers = [ 36 | "Development Status :: 4 - Beta", 37 | "Environment :: Web Environment", 38 | "Intended Audience :: Developers", 39 | "License :: OSI Approved :: BSD License", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Framework :: Django", 43 | ], 44 | ) --------------------------------------------------------------------------------