Refactor & prettify ./hooks/post_gen_project.py

This commit is contained in:
Nikita P. Shupeyko 2018-02-07 02:38:43 +03:00
parent d655bee114
commit b76c36229c
6 changed files with 225 additions and 202 deletions

View File

@ -22,7 +22,7 @@ DJANGO_ADMIN_URL n/a r'^admin/'
DJANGO_CACHES CACHES (default) locmem redis
DJANGO_DATABASES DATABASES (default) See code See code
DJANGO_DEBUG DEBUG True False
DJANGO_SECRET_KEY SECRET_KEY CHANGEME!!! raises error
DJANGO_SECRET_KEY SECRET_KEY !!!SET DJANGO_SECRET_KEY!!! raises error
DJANGO_SECURE_BROWSER_XSS_FILTER SECURE_BROWSER_XSS_FILTER n/a True
DJANGO_SECURE_SSL_REDIRECT SECURE_SSL_REDIRECT n/a True
DJANGO_SECURE_CONTENT_TYPE_NOSNIFF SECURE_CONTENT_TYPE_NOSNIFF n/a True

View File

@ -1,238 +1,252 @@
"""
Does the following:
NOTE:
the below code is to be maintained Python 2.x-compatible
as the whole Cookiecutter Django project initialization
can potentially be run in Python 2.x environment
(at least so we presume in `pre_gen_project.py`).
1. Generates and saves random secret key
2. Removes the taskapp if celery isn't going to be used
3. Removes the .idea directory if PyCharm isn't going to be used
4. Copy files from /docs/ to {{ cookiecutter.project_slug }}/docs/
TODO: this might have to be moved to a pre_gen_hook
A portion of this code was adopted from Django's standard crypto functions and
utilities, specifically:
https://github.com/django/django/blob/master/django/utils/crypto.py
TODO: ? restrict Cookiecutter Django project initialization to Python 3.x environments only
"""
from __future__ import print_function
import os
import random
import shutil
import string
import sys
# Get the root project directory
PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
# Use the system PRNG if possible
try:
# Inspired by
# https://github.com/django/django/blob/master/django/utils/crypto.py
random = random.SystemRandom()
using_sysrandom = True
except NotImplementedError:
using_sysrandom = False
def get_random_string(length=50):
"""
Returns a securely generated random string.
The default length of 12 with the a-z, A-Z, 0-9 character set returns
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
"""
punctuation = string.punctuation.replace('"', '').replace("'", '')
punctuation = punctuation.replace('\\', '')
if using_sysrandom:
return ''.join(random.choice(
string.digits + string.ascii_letters + punctuation
) for i in range(length))
print(
"Cookiecutter Django couldn't find a secure pseudo-random number generator on your system."
" Please change change your SECRET_KEY variables in conf/settings/local.py and env.example"
" manually."
)
return "CHANGEME!!"
_PROJECT_DIR_PATH = os.path.realpath(os.path.curdir)
def set_secret_key(setting_file_location):
# Open locals.py
with open(setting_file_location) as f:
file_ = f.read()
# Generate a SECRET_KEY that matches the Django standard
SECRET_KEY = get_random_string()
# Replace "CHANGEME!!!" with SECRET_KEY
file_ = file_.replace('CHANGEME!!!', SECRET_KEY, 1)
# Write the results to the locals.py module
with open(setting_file_location, 'w') as f:
f.write(file_)
def remove_file(file_path):
if os.path.exists(file_path):
os.remove(file_path)
def make_secret_key(project_directory):
"""Generates and saves random secret key"""
# Determine the local_setting_file_location
local_setting = os.path.join(
project_directory,
'config/settings/local.py'
)
# local.py settings file
set_secret_key(local_setting)
env_file = os.path.join(
project_directory,
'env.example'
)
# env.example file
set_secret_key(env_file)
def remove_open_source_project_only_files():
file_names = [
'CONTRIBUTORS.txt',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_file(file_name):
if os.path.exists(file_name):
os.remove(file_name)
def remove_gplv3_files():
file_names = [
'COPYING',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_task_app(project_directory):
"""Removes the taskapp if celery isn't going to be used"""
# Determine the local_setting_file_location
task_app_location = os.path.join(
PROJECT_DIRECTORY,
'{{ cookiecutter.project_slug }}/taskapp'
)
shutil.rmtree(task_app_location)
def remove_pycharm_files():
idea_dir_path = os.path.join(_PROJECT_DIR_PATH, '.idea')
if os.path.exists(idea_dir_path):
shutil.rmtree(idea_dir_path)
def remove_pycharm_dir(project_directory):
"""
Removes directories related to PyCharm
if it isn't going to be used
"""
idea_dir_location = os.path.join(PROJECT_DIRECTORY, '.idea/')
if os.path.exists(idea_dir_location):
shutil.rmtree(idea_dir_location)
docs_dir_location = os.path.join(PROJECT_DIRECTORY, 'docs/pycharm/')
if os.path.exists(docs_dir_location):
shutil.rmtree(docs_dir_location)
def remove_heroku_files():
"""
Removes files needed for heroku if it isn't going to be used
"""
filenames = ["Procfile", "runtime.txt"]
for filename in ["Procfile", "runtime.txt"]:
file_name = os.path.join(PROJECT_DIRECTORY, filename)
remove_file(file_name)
docs_dir_path = os.path.join(_PROJECT_DIR_PATH, 'docs', 'pycharm')
if os.path.exists(docs_dir_path):
shutil.rmtree(docs_dir_path)
def remove_docker_files():
"""
Removes files needed for docker if it isn't going to be used
"""
for filename in ["local.yml", "production.yml", ".dockerignore"]:
filename = os.path.join(PROJECT_DIRECTORY, filename)
if os.path.exists(filename):
os.remove(filename)
shutil.rmtree(os.path.join(_PROJECT_DIR_PATH, 'compose'))
shutil.rmtree(os.path.join(
PROJECT_DIRECTORY, "compose"
))
file_names = [
'local.yml',
'production.yml',
'.dockerignore',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_heroku_files():
file_names = [
'Procfile',
'runtime.txt',
]
for file_name in file_names:
remove_file(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_paas_files():
none_paas_files_left = True
if '{{ cookiecutter.use_heroku }}'.lower() == 'n':
remove_heroku_files()
none_paas_files_left &= True
else:
none_paas_files_left &= False
if none_paas_files_left:
remove_file(os.path.join(_PROJECT_DIR_PATH, 'requirements.txt'))
def remove_grunt_files():
"""
Removes files needed for grunt if it isn't going to be used
"""
for filename in ["Gruntfile.js"]:
os.remove(os.path.join(
PROJECT_DIRECTORY, filename
))
file_names = [
'Gruntfile.js',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_gulp_files():
file_names = [
'gulpfile.js',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_packagejson_file():
file_names = [
'package.json',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_celery_app():
shutil.rmtree(os.path.join(_PROJECT_DIR_PATH, '{{ cookiecutter.project_slug }}', 'taskapp'))
def append_to_gitignore(path):
gitignore_file_path = os.path.join(_PROJECT_DIR_PATH, '.gitignore')
with open(gitignore_file_path, 'a') as gitignore_file:
gitignore_file.write(path)
gitignore_file.write(os.linesep)
def generate_random_string(length,
using_digits=False,
using_ascii_letters=False,
using_punctuation=False):
"""
Removes files needed for grunt if it isn't going to be used
Example:
opting out for 50 symbol-long, [a-z][A-Z][0-9] string
would yield log_2((26+26+50)^50) ~= 334 bit strength.
"""
for filename in ["gulpfile.js"]:
os.remove(os.path.join(
PROJECT_DIRECTORY, filename
))
if not using_sysrandom:
return None
def remove_packageJSON_file():
"""
Removes files needed for grunt if it isn't going to be used
"""
for filename in ["package.json"]:
os.remove(os.path.join(
PROJECT_DIRECTORY, filename
))
def remove_copying_files():
"""
Removes files needed for the GPLv3 licence if it isn't going to be used
"""
for filename in ["COPYING"]:
os.remove(os.path.join(
PROJECT_DIRECTORY, filename
))
symbols = []
if using_digits:
symbols += string.digits
if using_ascii_letters:
symbols += string.ascii_letters
if using_punctuation:
symbols += string.punctuation \
.replace('"', '') \
.replace("'", '') \
.replace('\\', '')
return ''.join([random.choice(symbols) for _ in range(length)])
# IN PROGRESS
# def copy_doc_files(project_directory):
# cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir']
# cookiecutter_django_dir = os.path.join(
# cookiecutters_dir,
# 'cookiecutter-django',
# 'docs'
# )
# target_dir = os.path.join(
# project_directory,
# 'docs'
# )
# for name in os.listdir(cookiecutter_django_dir):
# if name.endswith('.rst') and not name.startswith('index'):
# src = os.path.join(cookiecutter_django_dir, name)
# dst = os.path.join(target_dir, name)
# shutil.copyfile(src, dst)
def replace_flag_with_random_string(file_path,
flag,
*args,
**kwargs):
random_string = generate_random_string(*args, **kwargs)
if random_string is None:
import sys
sys.stdout.write(
"We couldn't find a secure pseudo-random number generator on your system. "
"Please, {} manually.".format(flag)
)
random_string = flag
# 1. Generates and saves random secret key
make_secret_key(PROJECT_DIRECTORY)
# 2. Removes the taskapp if celery isn't going to be used
if '{{ cookiecutter.use_celery }}'.lower() == 'n':
remove_task_app(PROJECT_DIRECTORY)
# 3. Removes the .idea directory if PyCharm isn't going to be used
if '{{ cookiecutter.use_pycharm }}'.lower() != 'y':
remove_pycharm_dir(PROJECT_DIRECTORY)
# 4. Removes all heroku files if it isn't going to be used
if '{{ cookiecutter.use_heroku }}'.lower() != 'y':
remove_heroku_files()
# 5. Removes all docker files if it isn't going to be used
if '{{ cookiecutter.use_docker }}'.lower() != 'y':
remove_docker_files()
# 6. Removes all JS task manager files if it isn't going to be used
if '{{ cookiecutter.js_task_runner}}'.lower() == 'gulp':
remove_grunt_files()
elif '{{ cookiecutter.js_task_runner}}'.lower() == 'grunt':
remove_gulp_files()
else:
remove_gulp_files()
remove_grunt_files()
remove_packageJSON_file()
with open(file_path, 'r+') as f:
file_contents = f.read().replace(flag, random_string)
f.seek(0)
f.write(file_contents)
f.truncate()
# 9. Display a warning if use_docker and use_grunt are selected. Grunt isn't
# supported by our docker config atm.
if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] and '{{ cookiecutter.use_docker }}'.lower() == 'y':
print(
"You selected to use docker and a JS task runner. This is NOT supported out of the box for now. You "
"can continue to use the project like you normally would, but you will need to add a "
"js task runner service to your docker configuration manually."
def set_django_secret_key(file_path):
replace_flag_with_random_string(
file_path,
'!!!SET DJANGO_SECRET_KEY!!!',
length=50,
using_digits=True,
using_ascii_letters=True
)
# 10. Removes files needed for the GPLv3 licence if it isn't going to be used.
if '{{ cookiecutter.open_source_license}}' != 'GPLv3':
remove_copying_files()
def set_postgres_user(file_path):
replace_flag_with_random_string(
file_path,
'!!!SET POSTGRES_USER!!!',
length=8,
using_ascii_letters=True
)
def set_postgres_password(file_path):
replace_flag_with_random_string(
file_path,
'!!!SET POSTGRES_PASSWORD!!!',
length=42,
using_digits=True,
using_ascii_letters=True
)
def main():
if '{{ cookiecutter.open_source_license }}' == 'Not open source':
remove_open_source_project_only_files()
elif '{{ cookiecutter.open_source_license}}' != 'GPLv3':
remove_gplv3_files()
if '{{ cookiecutter.use_pycharm }}'.lower() == 'n':
remove_pycharm_files()
if '{{ cookiecutter.use_docker }}'.lower() == 'n':
remove_docker_files()
remove_paas_files()
if '{{ cookiecutter.js_task_runner}}'.lower() == 'gulp':
remove_grunt_files()
elif '{{ cookiecutter.js_task_runner}}'.lower() == 'grunt':
remove_gulp_files()
else:
remove_gulp_files()
remove_grunt_files()
remove_packagejson_file()
if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] \
and '{{ cookiecutter.use_docker }}'.lower() == 'y':
TERMINATOR = "\x1b[0m"
INFO = "\x1b[1;33m [INFO]: "
sys.stdout.write(
INFO +
"Docker and {} JS task runner ".format('{{ cookiecutter.js_task_runner }}'.lower().capitalize()) +
"working together not supported yet. "
"You can continue using the generated project like you normally would, "
"however you would need to add a JS task runner service "
"to your Docker Compose configuration manually." +
TERMINATOR
)
if '{{ cookiecutter.use_celery }}'.lower() == 'n':
remove_celery_app()
envexample_file_path = os.path.join(_PROJECT_DIR_PATH, 'env.example')
set_django_secret_key(os.path.join(_PROJECT_DIR_PATH, 'config', 'settings', 'local.py'))
set_django_secret_key(os.path.join(_PROJECT_DIR_PATH, 'config', 'settings', 'test.py'))
set_django_secret_key(envexample_file_path)
set_postgres_user(envexample_file_path)
set_postgres_password(envexample_file_path)
if __name__ == '__main__':
main()

View File

@ -1,3 +1,12 @@
"""
NOTE:
the below code is to be maintained Python 2.x-compatible
as the whole Cookiecutter Django project initialization
can potentially be run in Python 2.x environment.
TODO: ? restrict Cookiecutter Django project initialization to Python 3.x environments only
"""
project_slug = '{{ cookiecutter.project_slug }}'
if hasattr(project_slug, 'isidentifier'):
assert project_slug.isidentifier(), "'{}' project slug is not a valid Python identifier.".format(project_slug)

View File

@ -24,7 +24,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key only used for development and testing.
SECRET_KEY = env('DJANGO_SECRET_KEY', default='CHANGEME!!!')
SECRET_KEY = env('DJANGO_SECRET_KEY', default='!!!SET DJANGO_SECRET_KEY!!!')
# Mail settings
# ------------------------------------------------------------------------------

View File

@ -17,7 +17,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = False
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key only used for development and testing.
SECRET_KEY = env('DJANGO_SECRET_KEY', default='CHANGEME!!!')
SECRET_KEY = env('DJANGO_SECRET_KEY', default='!!!SET DJANGO_SECRET_KEY!!!')
# Mail settings
# ------------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
# PostgreSQL
POSTGRES_PASSWORD=mysecretpass
POSTGRES_USER=postgresuser
POSTGRES_PASSWORD=!!!SET POSTGRES_PASSWORD!!!
POSTGRES_USER=!!!SET POSTGRES_USER!!!
CONN_MAX_AGE=
# Domain name, used by caddy
@ -11,7 +11,7 @@ DOMAIN_NAME={{ cookiecutter.domain_name }}
# DJANGO_READ_DOT_ENV_FILE=True
DJANGO_ADMIN_URL=
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=CHANGEME!!!
DJANGO_SECRET_KEY=!!!SET DJANGO_SECRET_KEY!!!
DJANGO_ALLOWED_HOSTS=.{{ cookiecutter.domain_name }}
# AWS Settings