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_CACHES CACHES (default) locmem redis
DJANGO_DATABASES DATABASES (default) See code See code DJANGO_DATABASES DATABASES (default) See code See code
DJANGO_DEBUG DEBUG True False 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_BROWSER_XSS_FILTER SECURE_BROWSER_XSS_FILTER n/a True
DJANGO_SECURE_SSL_REDIRECT SECURE_SSL_REDIRECT 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 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 TODO: ? restrict Cookiecutter Django project initialization to Python 3.x environments only
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
""" """
from __future__ import print_function
import os import os
import random import random
import shutil import shutil
import string import string
import sys
# Get the root project directory
PROJECT_DIRECTORY = os.path.realpath(os.path.curdir)
# Use the system PRNG if possible
try: try:
# Inspired by
# https://github.com/django/django/blob/master/django/utils/crypto.py
random = random.SystemRandom() random = random.SystemRandom()
using_sysrandom = True using_sysrandom = True
except NotImplementedError: except NotImplementedError:
using_sysrandom = False using_sysrandom = False
_PROJECT_DIR_PATH = os.path.realpath(os.path.curdir)
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!!"
def set_secret_key(setting_file_location): def remove_file(file_path):
# Open locals.py if os.path.exists(file_path):
with open(setting_file_location) as f: os.remove(file_path)
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 make_secret_key(project_directory): def remove_open_source_project_only_files():
"""Generates and saves random secret key""" file_names = [
# Determine the local_setting_file_location 'CONTRIBUTORS.txt',
local_setting = os.path.join( ]
project_directory, for file_name in file_names:
'config/settings/local.py' os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
)
# 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_file(file_name): def remove_gplv3_files():
if os.path.exists(file_name): file_names = [
os.remove(file_name) 'COPYING',
]
for file_name in file_names:
os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
def remove_task_app(project_directory): def remove_pycharm_files():
"""Removes the taskapp if celery isn't going to be used""" idea_dir_path = os.path.join(_PROJECT_DIR_PATH, '.idea')
# Determine the local_setting_file_location if os.path.exists(idea_dir_path):
task_app_location = os.path.join( shutil.rmtree(idea_dir_path)
PROJECT_DIRECTORY,
'{{ cookiecutter.project_slug }}/taskapp'
)
shutil.rmtree(task_app_location)
docs_dir_path = os.path.join(_PROJECT_DIR_PATH, 'docs', 'pycharm')
def remove_pycharm_dir(project_directory): if os.path.exists(docs_dir_path):
""" shutil.rmtree(docs_dir_path)
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)
def remove_docker_files(): def remove_docker_files():
""" shutil.rmtree(os.path.join(_PROJECT_DIR_PATH, 'compose'))
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( file_names = [
PROJECT_DIRECTORY, "compose" '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(): def remove_grunt_files():
""" file_names = [
Removes files needed for grunt if it isn't going to be used 'Gruntfile.js',
""" ]
for filename in ["Gruntfile.js"]: for file_name in file_names:
os.remove(os.path.join( os.remove(os.path.join(_PROJECT_DIR_PATH, file_name))
PROJECT_DIRECTORY, filename
))
def remove_gulp_files(): 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"]: if not using_sysrandom:
os.remove(os.path.join( return None
PROJECT_DIRECTORY, filename
))
def remove_packageJSON_file(): symbols = []
""" if using_digits:
Removes files needed for grunt if it isn't going to be used symbols += string.digits
""" if using_ascii_letters:
for filename in ["package.json"]: symbols += string.ascii_letters
os.remove(os.path.join( if using_punctuation:
PROJECT_DIRECTORY, filename symbols += string.punctuation \
)) .replace('"', '') \
.replace("'", '') \
def remove_copying_files(): .replace('\\', '')
""" return ''.join([random.choice(symbols) for _ in range(length)])
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
))
# IN PROGRESS def replace_flag_with_random_string(file_path,
# def copy_doc_files(project_directory): flag,
# cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir'] *args,
# cookiecutter_django_dir = os.path.join( **kwargs):
# cookiecutters_dir, random_string = generate_random_string(*args, **kwargs)
# 'cookiecutter-django', if random_string is None:
# 'docs' import sys
# ) sys.stdout.write(
# target_dir = os.path.join( "We couldn't find a secure pseudo-random number generator on your system. "
# project_directory, "Please, {} manually.".format(flag)
# 'docs' )
# ) random_string = flag
# 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)
# 1. Generates and saves random secret key with open(file_path, 'r+') as f:
make_secret_key(PROJECT_DIRECTORY) file_contents = f.read().replace(flag, random_string)
f.seek(0)
# 2. Removes the taskapp if celery isn't going to be used f.write(file_contents)
if '{{ cookiecutter.use_celery }}'.lower() == 'n': f.truncate()
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()
# 9. Display a warning if use_docker and use_grunt are selected. Grunt isn't def set_django_secret_key(file_path):
# supported by our docker config atm. replace_flag_with_random_string(
if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] and '{{ cookiecutter.use_docker }}'.lower() == 'y': file_path,
print( '!!!SET DJANGO_SECRET_KEY!!!',
"You selected to use docker and a JS task runner. This is NOT supported out of the box for now. You " length=50,
"can continue to use the project like you normally would, but you will need to add a " using_digits=True,
"js task runner service to your docker configuration manually." 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': def set_postgres_user(file_path):
remove_copying_files() 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 }}' project_slug = '{{ cookiecutter.project_slug }}'
if hasattr(project_slug, 'isidentifier'): if hasattr(project_slug, 'isidentifier'):
assert project_slug.isidentifier(), "'{}' project slug is not a valid Python identifier.".format(project_slug) 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 # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key only used for development and testing. # 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 # Mail settings
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -17,7 +17,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = False
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key only used for development and testing. # 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 # Mail settings
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

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