First pass at running black across the project (#1602)

This commit is contained in:
Daniel Roy Greenfeld 2018-04-08 17:03:29 -05:00 committed by GitHub
parent 4bdfbb4307
commit 3f753e0411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 499 additions and 433 deletions

View File

@ -29,19 +29,19 @@ now = datetime.now()
extensions = [] extensions = []
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = 'Cookiecutter Django' project = "Cookiecutter Django"
copyright = "2013-2018, Daniel Roy Greenfeld".format(now.year) copyright = "2013-2018, Daniel Roy Greenfeld".format(now.year)
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -49,9 +49,9 @@ copyright = "2013-2018, Daniel Roy Greenfeld".format(now.year)
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '{}.{}.{}'.format(*now.isocalendar()) version = "{}.{}.{}".format(*now.isocalendar())
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '{}.{}.{}'.format(*now.isocalendar()) release = "{}.{}.{}".format(*now.isocalendar())
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -65,7 +65,7 @@ release = '{}.{}.{}'.format(*now.isocalendar())
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None # default_role = None
@ -82,7 +82,7 @@ exclude_patterns = ['_build']
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@ -92,7 +92,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'default' html_theme = "default"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@ -121,7 +121,7 @@ html_theme = 'default'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
@ -165,7 +165,7 @@ html_static_path = ['_static']
# html_file_suffix = None # html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'cookiecutter-djangodoc' htmlhelp_basename = "cookiecutter-djangodoc"
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
@ -173,10 +173,8 @@ htmlhelp_basename = 'cookiecutter-djangodoc'
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# 'preamble': '', # 'preamble': '',
} }
@ -184,10 +182,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', (
'cookiecutter-django.tex', "index",
'cookiecutter-django Documentation', "cookiecutter-django.tex",
'cookiecutter-django', 'manual'), "cookiecutter-django Documentation",
"cookiecutter-django",
"manual",
)
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -216,8 +217,13 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'Cookiecutter Django', 'Cookiecutter Django documentation', (
['Daniel Roy Greenfeld'], 1) "index",
"Cookiecutter Django",
"Cookiecutter Django documentation",
["Daniel Roy Greenfeld"],
1,
)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@ -230,9 +236,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'Cookiecutter Django', 'Cookiecutter Django documentation', (
'Daniel Roy Greenfeld', 'Cookiecutter Django', "index",
'A Cookiecutter template for creating production-ready Django projects quickly.', 'Miscellaneous'), "Cookiecutter Django",
"Cookiecutter Django documentation",
"Daniel Roy Greenfeld",
"Cookiecutter Django",
"A Cookiecutter template for creating production-ready Django projects quickly.",
"Miscellaneous",
)
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.

View File

@ -30,96 +30,77 @@ SUCCESS = "\x1b[1;32m [SUCCESS]: "
def remove_open_source_files(): def remove_open_source_files():
file_names = [ file_names = ["CONTRIBUTORS.txt"]
'CONTRIBUTORS.txt',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_gplv3_files(): def remove_gplv3_files():
file_names = [ file_names = ["COPYING"]
'COPYING',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_pycharm_files(): def remove_pycharm_files():
idea_dir_path = '.idea' idea_dir_path = ".idea"
if os.path.exists(idea_dir_path): if os.path.exists(idea_dir_path):
shutil.rmtree(idea_dir_path) shutil.rmtree(idea_dir_path)
docs_dir_path = os.path.join('docs', 'pycharm') docs_dir_path = os.path.join("docs", "pycharm")
if os.path.exists(docs_dir_path): if os.path.exists(docs_dir_path):
shutil.rmtree(docs_dir_path) shutil.rmtree(docs_dir_path)
def remove_docker_files(): def remove_docker_files():
shutil.rmtree('compose') shutil.rmtree("compose")
file_names = [ file_names = ["local.yml", "production.yml", ".dockerignore"]
'local.yml',
'production.yml',
'.dockerignore',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_heroku_files(): def remove_heroku_files():
file_names = [ file_names = ["Procfile", "runtime.txt", "requirements.txt"]
'Procfile',
'runtime.txt',
'requirements.txt',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_grunt_files(): def remove_grunt_files():
file_names = [ file_names = ["Gruntfile.js"]
'Gruntfile.js',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_gulp_files(): def remove_gulp_files():
file_names = [ file_names = ["gulpfile.js"]
'gulpfile.js',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_packagejson_file(): def remove_packagejson_file():
file_names = [ file_names = ["package.json"]
'package.json',
]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
def remove_celery_app(): def remove_celery_app():
shutil.rmtree(os.path.join('{{ cookiecutter.project_slug }}', 'taskapp')) shutil.rmtree(os.path.join("{{ cookiecutter.project_slug }}", "taskapp"))
def remove_dottravisyml_file(): def remove_dottravisyml_file():
os.remove('.travis.yml') os.remove(".travis.yml")
def append_to_project_gitignore(path): def append_to_project_gitignore(path):
gitignore_file_path = '.gitignore' gitignore_file_path = ".gitignore"
with open(gitignore_file_path, 'a') as gitignore_file: with open(gitignore_file_path, "a") as gitignore_file:
gitignore_file.write(path) gitignore_file.write(path)
gitignore_file.write(os.linesep) gitignore_file.write(os.linesep)
def generate_random_string(length, def generate_random_string(
using_digits=False, length, using_digits=False, using_ascii_letters=False, using_punctuation=False
using_ascii_letters=False, ):
using_punctuation=False):
""" """
Example: Example:
opting out for 50 symbol-long, [a-z][A-Z][0-9] string opting out for 50 symbol-long, [a-z][A-Z][0-9] string
@ -134,19 +115,13 @@ def generate_random_string(length,
if using_ascii_letters: if using_ascii_letters:
symbols += string.ascii_letters symbols += string.ascii_letters
if using_punctuation: if using_punctuation:
symbols += string.punctuation \ symbols += string.punctuation.replace('"', "").replace("'", "").replace(
.replace('"', '') \ "\\", ""
.replace("'", '') \ )
.replace('\\', '') return "".join([random.choice(symbols) for _ in range(length)])
return ''.join([random.choice(symbols) for _ in range(length)])
def set_flag(file_path, def set_flag(file_path, flag, value=None, formatted=None, *args, **kwargs):
flag,
value=None,
formatted=None,
*args,
**kwargs):
if value is None: if value is None:
random_string = generate_random_string(*args, **kwargs) random_string = generate_random_string(*args, **kwargs)
if random_string is None: if random_string is None:
@ -159,7 +134,7 @@ def set_flag(file_path,
random_string = formatted.format(random_string) random_string = formatted.format(random_string)
value = random_string value = random_string
with open(file_path, 'r+') as f: with open(file_path, "r+") as f:
file_contents = f.read().replace(flag, value) file_contents = f.read().replace(flag, value)
f.seek(0) f.seek(0)
f.write(file_contents) f.write(file_contents)
@ -171,10 +146,10 @@ def set_flag(file_path,
def set_django_secret_key(file_path): def set_django_secret_key(file_path):
django_secret_key = set_flag( django_secret_key = set_flag(
file_path, file_path,
'!!!SET DJANGO_SECRET_KEY!!!', "!!!SET DJANGO_SECRET_KEY!!!",
length=64, length=64,
using_digits=True, using_digits=True,
using_ascii_letters=True using_ascii_letters=True,
) )
return django_secret_key return django_secret_key
@ -182,28 +157,22 @@ def set_django_secret_key(file_path):
def set_django_admin_url(file_path): def set_django_admin_url(file_path):
django_admin_url = set_flag( django_admin_url = set_flag(
file_path, file_path,
'!!!SET DJANGO_ADMIN_URL!!!', "!!!SET DJANGO_ADMIN_URL!!!",
formatted='^{}/', formatted="^{}/",
length=32, length=32,
using_digits=True, using_digits=True,
using_ascii_letters=True using_ascii_letters=True,
) )
return django_admin_url return django_admin_url
def generate_postgres_user(): def generate_postgres_user():
return generate_random_string( return generate_random_string(length=32, using_ascii_letters=True)
length=32,
using_ascii_letters=True
)
def set_postgres_user(file_path, def set_postgres_user(file_path, value=None):
value=None):
postgres_user = set_flag( postgres_user = set_flag(
file_path, file_path, "!!!SET POSTGRES_USER!!!", value=value or generate_postgres_user()
'!!!SET POSTGRES_USER!!!',
value=value or generate_postgres_user()
) )
return postgres_user return postgres_user
@ -211,47 +180,47 @@ def set_postgres_user(file_path,
def set_postgres_password(file_path): def set_postgres_password(file_path):
postgres_password = set_flag( postgres_password = set_flag(
file_path, file_path,
'!!!SET POSTGRES_PASSWORD!!!', "!!!SET POSTGRES_PASSWORD!!!",
length=64, length=64,
using_digits=True, using_digits=True,
using_ascii_letters=True using_ascii_letters=True,
) )
return postgres_password return postgres_password
def append_to_gitignore_file(s): def append_to_gitignore_file(s):
with open('.gitignore', 'a') as gitignore_file: with open(".gitignore", "a") as gitignore_file:
gitignore_file.write(s) gitignore_file.write(s)
gitignore_file.write(os.linesep) gitignore_file.write(os.linesep)
def set_flags_in_envs(postgres_user): def set_flags_in_envs(postgres_user):
local_postgres_envs_path = os.path.join('.envs', '.local', '.postgres') local_postgres_envs_path = os.path.join(".envs", ".local", ".postgres")
set_postgres_user(local_postgres_envs_path, value=postgres_user) set_postgres_user(local_postgres_envs_path, value=postgres_user)
set_postgres_password(local_postgres_envs_path) set_postgres_password(local_postgres_envs_path)
production_django_envs_path = os.path.join('.envs', '.production', '.django') production_django_envs_path = os.path.join(".envs", ".production", ".django")
set_django_secret_key(production_django_envs_path) set_django_secret_key(production_django_envs_path)
set_django_admin_url(production_django_envs_path) set_django_admin_url(production_django_envs_path)
production_postgres_envs_path = os.path.join('.envs', '.production', '.postgres') production_postgres_envs_path = os.path.join(".envs", ".production", ".postgres")
set_postgres_user(production_postgres_envs_path, value=postgres_user) set_postgres_user(production_postgres_envs_path, value=postgres_user)
set_postgres_password(production_postgres_envs_path) set_postgres_password(production_postgres_envs_path)
def set_flags_in_settings_files(): def set_flags_in_settings_files():
set_django_secret_key(os.path.join('config', 'settings', 'local.py')) set_django_secret_key(os.path.join("config", "settings", "local.py"))
set_django_secret_key(os.path.join('config', 'settings', 'test.py')) set_django_secret_key(os.path.join("config", "settings", "test.py"))
def remove_envs_and_associated_files(): def remove_envs_and_associated_files():
shutil.rmtree('.envs') shutil.rmtree(".envs")
os.remove('merge_production_dotenvs_in_dotenv.py') os.remove("merge_production_dotenvs_in_dotenv.py")
def remove_celery_compose_dirs(): def remove_celery_compose_dirs():
shutil.rmtree(os.path.join('compose', 'local', 'django', 'celery')) shutil.rmtree(os.path.join("compose", "local", "django", "celery"))
shutil.rmtree(os.path.join('compose', 'production', 'django', 'celery')) shutil.rmtree(os.path.join("compose", "production", "django", "celery"))
def main(): def main():
@ -259,75 +228,67 @@ def main():
set_flags_in_envs(postgres_user) set_flags_in_envs(postgres_user)
set_flags_in_settings_files() set_flags_in_settings_files()
if '{{ cookiecutter.open_source_license }}' == 'Not open source': if "{{ cookiecutter.open_source_license }}" == "Not open source":
remove_open_source_files() remove_open_source_files()
if '{{ cookiecutter.open_source_license}}' != 'GPLv3': if "{{ cookiecutter.open_source_license}}" != "GPLv3":
remove_gplv3_files() remove_gplv3_files()
if '{{ cookiecutter.use_pycharm }}'.lower() == 'n': if "{{ cookiecutter.use_pycharm }}".lower() == "n":
remove_pycharm_files() remove_pycharm_files()
if '{{ cookiecutter.use_docker }}'.lower() == 'n': if "{{ cookiecutter.use_docker }}".lower() == "n":
remove_docker_files() remove_docker_files()
if '{{ cookiecutter.use_heroku }}'.lower() == 'n': if "{{ cookiecutter.use_heroku }}".lower() == "n":
remove_heroku_files() remove_heroku_files()
if '{{ cookiecutter.use_docker }}'.lower() == 'n' and '{{ cookiecutter.use_heroku }}'.lower() == 'n': if "{{ cookiecutter.use_docker }}".lower() == "n" and "{{ cookiecutter.use_heroku }}".lower() == "n":
if '{{ cookiecutter.keep_local_envs_in_vcs }}'.lower() == 'y': if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
print( print(
INFO + INFO + ".env(s) are only utilized when Docker Compose and/or "
".env(s) are only utilized when Docker Compose and/or "
"Heroku support is enabled so keeping them does not " "Heroku support is enabled so keeping them does not "
"make sense given your current setup." + "make sense given your current setup." + TERMINATOR
TERMINATOR
) )
remove_envs_and_associated_files() remove_envs_and_associated_files()
else: else:
append_to_gitignore_file('.env') append_to_gitignore_file(".env")
append_to_gitignore_file('.envs/*') append_to_gitignore_file(".envs/*")
if '{{ cookiecutter.keep_local_envs_in_vcs }}'.lower() == 'y': if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
append_to_gitignore_file('!.envs/.local/') append_to_gitignore_file("!.envs/.local/")
if '{{ cookiecutter.js_task_runner}}'.lower() == 'gulp': if "{{ cookiecutter.js_task_runner}}".lower() == "gulp":
remove_grunt_files() remove_grunt_files()
elif '{{ cookiecutter.js_task_runner}}'.lower() == 'grunt': elif "{{ cookiecutter.js_task_runner}}".lower() == "grunt":
remove_gulp_files() remove_gulp_files()
else: else:
remove_gulp_files() remove_gulp_files()
remove_grunt_files() remove_grunt_files()
remove_packagejson_file() remove_packagejson_file()
if '{{ cookiecutter.js_task_runner }}'.lower() in ['grunt', 'gulp'] \ if "{{ cookiecutter.js_task_runner }}".lower() in [
and '{{ cookiecutter.use_docker }}'.lower() == 'y': "grunt", "gulp"
] and "{{ cookiecutter.use_docker }}".lower() == "y":
print( print(
WARNING + WARNING
"Docker and {} JS task runner ".format( + "Docker and {} JS task runner ".format(
'{{ cookiecutter.js_task_runner }}' "{{ cookiecutter.js_task_runner }}".lower().capitalize()
.lower() )
.capitalize() + "working together not supported yet. "
) +
"working together not supported yet. "
"You can continue using the generated project like you " "You can continue using the generated project like you "
"normally would, however you would need to add a JS " "normally would, however you would need to add a JS "
"task runner service to your Docker Compose configuration " "task runner service to your Docker Compose configuration "
"manually." + "manually." + TERMINATOR
TERMINATOR
) )
if '{{ cookiecutter.use_celery }}'.lower() == 'n': if "{{ cookiecutter.use_celery }}".lower() == "n":
remove_celery_app() remove_celery_app()
if '{{ cookiecutter.use_docker }}'.lower() == 'y': if "{{ cookiecutter.use_docker }}".lower() == "y":
remove_celery_compose_dirs() remove_celery_compose_dirs()
if '{{ cookiecutter.use_travisci }}'.lower() == 'n': if "{{ cookiecutter.use_travisci }}".lower() == "n":
remove_dottravisyml_file() remove_dottravisyml_file()
print( print(SUCCESS + "Project initialized, keep up the good work!" + TERMINATOR)
SUCCESS +
"Project initialized, keep up the good work!" +
TERMINATOR
)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -16,40 +16,41 @@ INFO = "\x1b[1;33m [INFO]: "
HINT = "\x1b[3;33m" HINT = "\x1b[3;33m"
SUCCESS = "\x1b[1;32m [SUCCESS]: " SUCCESS = "\x1b[1;32m [SUCCESS]: "
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
)
assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name." assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name."
if '{{ cookiecutter.use_docker }}'.lower() == 'n': if "{{ cookiecutter.use_docker }}".lower() == "n":
python_major_version = sys.version_info[0] python_major_version = sys.version_info[0]
if python_major_version == 2: if python_major_version == 2:
print( print(
WARNING + WARNING + "Cookiecutter Django does not support Python 2. "
"Cookiecutter Django does not support Python 2. "
"Stability is guaranteed with Python 3.6+ only, " "Stability is guaranteed with Python 3.6+ only, "
"are you sure you want to proceed (y/n)? " + "are you sure you want to proceed (y/n)? " + TERMINATOR
TERMINATOR
) )
yes_options, no_options = frozenset(['y']), frozenset(['n']) yes_options, no_options = frozenset(["y"]), frozenset(["n"])
while True: while True:
choice = raw_input().lower() choice = raw_input().lower()
if choice in yes_options: if choice in yes_options:
break break
elif choice in no_options: elif choice in no_options:
print( print(INFO + "Generation process stopped as requested." + TERMINATOR)
INFO +
"Generation process stopped as requested." +
TERMINATOR
)
sys.exit(1) sys.exit(1)
else: else:
print( print(
HINT + HINT
"Please respond with {} or {}: ".format( + "Please respond with {} or {}: ".format(
', '.join(["'{}'".format(o) for o in yes_options if not o == '']), ", ".join(
', '.join(["'{}'".format(o) for o in no_options if not o == '']) ["'{}'".format(o) for o in yes_options if not o == ""]
) + ),
TERMINATOR ", ".join(
["'{}'".format(o) for o in no_options if not o == ""]
),
)
+ TERMINATOR
) )

View File

@ -10,42 +10,42 @@ except ImportError:
# Our version ALWAYS matches the version of Django we support # Our version ALWAYS matches the version of Django we support
# If Django has a new release, we branch, tag, then update this setting after the tag. # If Django has a new release, we branch, tag, then update this setting after the tag.
version = '2.0.2' version = "2.0.2"
if sys.argv[-1] == 'tag': if sys.argv[-1] == "tag":
os.system('git tag -a %s -m "version %s"' % (version, version)) os.system('git tag -a %s -m "version %s"' % (version, version))
os.system('git push --tags') os.system("git push --tags")
sys.exit() sys.exit()
with open('README.rst') as readme_file: with open("README.rst") as readme_file:
long_description = readme_file.read() long_description = readme_file.read()
setup( setup(
name='cookiecutter-django', name="cookiecutter-django",
version=version, version=version,
description='A Cookiecutter template for creating production-ready Django projects quickly.', description="A Cookiecutter template for creating production-ready Django projects quickly.",
long_description=long_description, long_description=long_description,
author='Daniel Roy Greenfeld', author="Daniel Roy Greenfeld",
author_email='pydanny@gmail.com', author_email="pydanny@gmail.com",
url='https://github.com/pydanny/cookiecutter-django', url="https://github.com/pydanny/cookiecutter-django",
packages=[], packages=[],
license='BSD', license="BSD",
zip_safe=False, zip_safe=False,
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', "Development Status :: 4 - Beta",
'Environment :: Console', "Environment :: Console",
'Framework :: Django :: 2.0', "Framework :: Django :: 2.0",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Natural Language :: English', "Natural Language :: English",
'License :: OSI Approved :: BSD License', "License :: OSI Approved :: BSD License",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: Implementation :: CPython', "Programming Language :: Python :: Implementation :: CPython",
'Topic :: Software Development', "Topic :: Software Development",
], ],
keywords=( keywords=(
'cookiecutter, Python, projects, project templates, django, ' "cookiecutter, Python, projects, project templates, django, "
'skeleton, scaffolding, project directory, setup.py' "skeleton, scaffolding, project directory, setup.py"
), ),
) )

View File

@ -5,21 +5,21 @@ import sh
import pytest import pytest
from binaryornot.check import is_binary from binaryornot.check import is_binary
PATTERN = '{{(\s?cookiecutter)[.](.*?)}}' PATTERN = "{{(\s?cookiecutter)[.](.*?)}}"
RE_OBJ = re.compile(PATTERN) RE_OBJ = re.compile(PATTERN)
@pytest.fixture @pytest.fixture
def context(): def context():
return { return {
'project_name': 'My Test Project', "project_name": "My Test Project",
'project_slug': 'my_test_project', "project_slug": "my_test_project",
'author_name': 'Test Author', "author_name": "Test Author",
'email': 'test@example.com', "email": "test@example.com",
'description': 'A short description of the project.', "description": "A short description of the project.",
'domain_name': 'example.com', "domain_name": "example.com",
'version': '0.1.0', "version": "0.1.0",
'timezone': 'UTC', "timezone": "UTC",
} }
@ -40,9 +40,10 @@ def check_paths(paths):
for path in paths: for path in paths:
if is_binary(path): if is_binary(path):
continue continue
for line in open(path, 'r'):
for line in open(path, "r"):
match = RE_OBJ.search(line) match = RE_OBJ.search(line)
msg = 'cookiecutter variable not replaced in {}' msg = "cookiecutter variable not replaced in {}"
assert match is None, msg.format(path) assert match is None, msg.format(path)
@ -50,7 +51,7 @@ def test_default_configuration(cookies, context):
result = cookies.bake(extra_context=context) result = cookies.bake(extra_context=context)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.exception is None assert result.exception is None
assert result.project.basename == context['project_slug'] assert result.project.basename == context["project_slug"]
assert result.project.isdir() assert result.project.isdir()
paths = build_files_list(str(result.project)) paths = build_files_list(str(result.project))
@ -58,9 +59,9 @@ def test_default_configuration(cookies, context):
check_paths(paths) check_paths(paths)
@pytest.fixture(params=['use_mailhog', 'use_celery', 'windows']) @pytest.fixture(params=["use_mailhog", "use_celery", "windows"])
def feature_context(request, context): def feature_context(request, context):
context.update({request.param: 'y'}) context.update({request.param: "y"})
return context return context
@ -68,7 +69,7 @@ def test_enabled_features(cookies, feature_context):
result = cookies.bake(extra_context=feature_context) result = cookies.bake(extra_context=feature_context)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.exception is None assert result.exception is None
assert result.project.basename == feature_context['project_slug'] assert result.project.basename == feature_context["project_slug"]
assert result.project.isdir() assert result.project.isdir()
paths = build_files_list(str(result.project)) paths = build_files_list(str(result.project))

View File

@ -10,47 +10,44 @@ from .base import env
# https://docs.djangoproject.com/en/dev/ref/settings/#debug # https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = False DEBUG = False
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env('DJANGO_SECRET_KEY', default='!!!SET DJANGO_SECRET_KEY!!!') SECRET_KEY = env("DJANGO_SECRET_KEY", default="!!!SET DJANGO_SECRET_KEY!!!")
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = 'django.test.runner.DiscoverRunner' TEST_RUNNER = "django.test.runner.DiscoverRunner"
# CACHES # CACHES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches # https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = { CACHES = {
'default': { "default": {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": ""
'LOCATION': ''
} }
} }
# PASSWORDS # PASSWORDS
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [ PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# TEMPLATES # TEMPLATES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates # https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405 TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
TEMPLATES[0]['OPTIONS']['loaders'] = [ # noqa F405 TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
( (
'django.template.loaders.cached.Loader', "django.template.loaders.cached.Loader",
[ [
'django.template.loaders.filesystem.Loader', "django.template.loaders.filesystem.Loader",
'django.template.loaders.app_directories.Loader', "django.template.loaders.app_directories.Loader",
], ],
), )
] ]
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-host # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
EMAIL_HOST = 'localhost' EMAIL_HOST = "localhost"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025 EMAIL_PORT = 1025

View File

@ -6,32 +6,47 @@ from django.views.generic import TemplateView
from django.views import defaults as default_views from django.views import defaults as default_views
urlpatterns = [ urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='pages/home.html'), name='home'), url(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"),
url(r'^about/$', TemplateView.as_view(template_name='pages/about.html'), name='about'), url(
r"^about/$",
TemplateView.as_view(template_name="pages/about.html"),
name="about",
),
# Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %} # Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %}
url(settings.ADMIN_URL, admin.site.urls), url(settings.ADMIN_URL, admin.site.urls),
# User management # User management
url(r'^users/', include('{{ cookiecutter.project_slug }}.users.urls', namespace='users')), url(
url(r'^accounts/', include('allauth.urls')), r"^users/",
include("{{ cookiecutter.project_slug }}.users.urls", namespace="users"),
),
url(r"^accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here # Your stuff: custom urls includes go here
] + static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) )
if settings.DEBUG: if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit # This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like. # these url in browser to see how these error pages look like.
urlpatterns += [ urlpatterns += [
url(r'^400/$', default_views.bad_request, kwargs={'exception': Exception('Bad Request!')}), url(
url(r'^403/$', default_views.permission_denied, kwargs={'exception': Exception('Permission Denied')}), r"^400/$",
url(r'^404/$', default_views.page_not_found, kwargs={'exception': Exception('Page not Found')}), default_views.bad_request,
url(r'^500/$', default_views.server_error), kwargs={"exception": Exception("Bad Request!")},
),
url(
r"^403/$",
default_views.permission_denied,
kwargs={"exception": Exception("Permission Denied")},
),
url(
r"^404/$",
default_views.page_not_found,
kwargs={"exception": Exception("Page not Found")},
),
url(r"^500/$", default_views.server_error),
] ]
if 'debug_toolbar' in settings.INSTALLED_APPS: if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)), urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns
] + urlpatterns

View File

@ -27,19 +27,19 @@ import sys
extensions = [] extensions = []
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = '{{ cookiecutter.project_name }}' project = "{{ cookiecutter.project_name }}"
copyright = """{% now 'utc', '%Y' %}, {{ cookiecutter.author_name }}""" copyright = """{% now 'utc', '%Y' %}, {{ cookiecutter.author_name }}"""
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -47,9 +47,9 @@ copyright = """{% now 'utc', '%Y' %}, {{ cookiecutter.author_name }}"""
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.1' version = "0.1"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.1' release = "0.1"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -63,7 +63,7 @@ release = '0.1'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None # default_role = None
@ -80,7 +80,7 @@ exclude_patterns = ['_build']
# show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] # modindex_common_prefix = []
@ -90,7 +90,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'default' html_theme = "default"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@ -119,7 +119,7 @@ html_theme = 'default'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
@ -163,7 +163,7 @@ html_static_path = ['_static']
# html_file_suffix = None # html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = '{{ cookiecutter.project_slug }}doc' htmlhelp_basename = "{{ cookiecutter.project_slug }}doc"
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
@ -171,10 +171,8 @@ htmlhelp_basename = '{{ cookiecutter.project_slug }}doc'
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# 'preamble': '', # 'preamble': '',
} }
@ -182,10 +180,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', (
'{{ cookiecutter.project_slug }}.tex', "index",
'{{ cookiecutter.project_name }} Documentation', "{{ cookiecutter.project_slug }}.tex",
"""{{ cookiecutter.author_name }}""", 'manual'), "{{ cookiecutter.project_name }} Documentation",
"""{{ cookiecutter.author_name }}""",
"manual",
)
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -214,8 +215,13 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', '{{ cookiecutter.project_slug }}', '{{ cookiecutter.project_name }} Documentation', (
["""{{ cookiecutter.author_name }}"""], 1) "index",
"{{ cookiecutter.project_slug }}",
"{{ cookiecutter.project_name }} Documentation",
["""{{ cookiecutter.author_name }}"""],
1,
)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@ -228,9 +234,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', '{{ cookiecutter.project_slug }}', '{{ cookiecutter.project_name }} Documentation', (
"""{{ cookiecutter.author_name }}""", '{{ cookiecutter.project_name }}', "index",
"""{{ cookiecutter.description }}""", 'Miscellaneous'), "{{ cookiecutter.project_slug }}",
"{{ cookiecutter.project_name }} Documentation",
"""{{ cookiecutter.author_name }}""",
"{{ cookiecutter.project_name }}",
"""{{ cookiecutter.description }}""",
"Miscellaneous",
)
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.

View File

@ -2,8 +2,8 @@
import os import os
import sys import sys
if __name__ == '__main__': if __name__ == "__main__":
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
@ -19,11 +19,12 @@ if __name__ == '__main__':
"available on your PYTHONPATH environment variable? Did you " "available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?" "forget to activate a virtual environment?"
) )
raise raise
# This allows easy placement of apps within the interior # This allows easy placement of apps within the interior
# {{ cookiecutter.project_slug }} directory. # {{ cookiecutter.project_slug }} directory.
current_path = os.path.dirname(os.path.abspath(__file__)) current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, '{{ cookiecutter.project_slug }}')) sys.path.append(os.path.join(current_path, "{{ cookiecutter.project_slug }}"))
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View File

@ -4,21 +4,21 @@ from typing import Sequence
import pytest import pytest
ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__)) ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__))
PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, '.envs', '.production') PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production")
PRODUCTION_DOTENV_FILE_PATHS = [ PRODUCTION_DOTENV_FILE_PATHS = [
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.django'), os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.postgres'), os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.caddy'), os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".caddy"),
] ]
DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, '.env') DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, ".env")
def merge(output_file_path: str, def merge(
merged_file_paths: Sequence[str], output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True
append_linesep: bool = True) -> None: ) -> None:
with open(output_file_path, 'w') as output_file: with open(output_file_path, "w") as output_file:
for merged_file_path in merged_file_paths: for merged_file_path in merged_file_paths:
with open(merged_file_path, 'r') as merged_file: with open(merged_file_path, "r") as merged_file:
merged_file_content = merged_file.read() merged_file_content = merged_file.read()
output_file.write(merged_file_content) output_file.write(merged_file_content)
if append_linesep: if append_linesep:
@ -29,26 +29,24 @@ def main():
merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS)
@pytest.mark.parametrize('merged_file_count', range(3)) @pytest.mark.parametrize("merged_file_count", range(3))
@pytest.mark.parametrize('append_linesep', [True, False]) @pytest.mark.parametrize("append_linesep", [True, False])
def test_merge(tmpdir_factory, def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
merged_file_count: int,
append_linesep: bool):
tmp_dir_path = str(tmpdir_factory.getbasetemp()) tmp_dir_path = str(tmpdir_factory.getbasetemp())
output_file_path = os.path.join(tmp_dir_path, '.env') output_file_path = os.path.join(tmp_dir_path, ".env")
expected_output_file_content = '' expected_output_file_content = ""
merged_file_paths = [] merged_file_paths = []
for i in range(merged_file_count): for i in range(merged_file_count):
merged_file_ord = i + 1 merged_file_ord = i + 1
merged_filename = '.service{}'.format(merged_file_ord) merged_filename = ".service{}".format(merged_file_ord)
merged_file_path = os.path.join(tmp_dir_path, merged_filename) merged_file_path = os.path.join(tmp_dir_path, merged_filename)
merged_file_content = merged_filename * merged_file_ord merged_file_content = merged_filename * merged_file_ord
with open(merged_file_path, 'w+') as file: with open(merged_file_path, "w+") as file:
file.write(merged_file_content) file.write(merged_file_content)
expected_output_file_content += merged_file_content expected_output_file_content += merged_file_content
@ -59,11 +57,11 @@ def test_merge(tmpdir_factory,
merge(output_file_path, merged_file_paths, append_linesep) merge(output_file_path, merged_file_paths, append_linesep)
with open(output_file_path, 'r') as output_file: with open(output_file_path, "r") as output_file:
actual_output_file_content = output_file.read() actual_output_file_content = output_file.read()
assert actual_output_file_content == expected_output_file_content assert actual_output_file_content == expected_output_file_content
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -1,2 +1,7 @@
__version__ = '{{ cookiecutter.version }}' __version__ = "{{ cookiecutter.version }}"
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) __version_info__ = tuple(
[
int(num) if num.isdigit() else num
for num in __version__.replace("-", ".", 1).split(".")
]
)

View File

@ -9,23 +9,34 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Site', name="Site",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
('domain', models.CharField( "id",
max_length=100, verbose_name='domain name', validators=[_simple_domain_name_validator] models.AutoField(
)), verbose_name="ID",
('name', models.CharField(max_length=50, verbose_name='display name')), serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"domain",
models.CharField(
max_length=100,
verbose_name="domain name",
validators=[_simple_domain_name_validator],
),
),
("name", models.CharField(max_length=50, verbose_name="display name")),
], ],
options={ options={
'ordering': ('domain',), "ordering": ("domain",),
'db_table': 'django_site', "db_table": "django_site",
'verbose_name': 'site', "verbose_name": "site",
'verbose_name_plural': 'sites', "verbose_name_plural": "sites",
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[ managers=[("objects", django.contrib.sites.models.SiteManager())],
('objects', django.contrib.sites.models.SiteManager()), )
],
),
] ]

View File

@ -4,17 +4,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("sites", "0001_initial")]
('sites', '0001_initial'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='site', model_name="site",
name='domain', name="domain",
field=models.CharField( field=models.CharField(
max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], max_length=100,
verbose_name='domain name' unique=True,
), validators=[django.contrib.sites.models._simple_domain_name_validator],
verbose_name="domain name",
), ),
)
] ]

View File

@ -9,34 +9,26 @@ from django.db import migrations
def update_site_forward(apps, schema_editor): def update_site_forward(apps, schema_editor):
"""Set site domain and name.""" """Set site domain and name."""
Site = apps.get_model('sites', 'Site') Site = apps.get_model("sites", "Site")
Site.objects.update_or_create( Site.objects.update_or_create(
id=settings.SITE_ID, id=settings.SITE_ID,
defaults={ defaults={
'domain': '{{cookiecutter.domain_name}}', "domain": "{{cookiecutter.domain_name}}",
'name': '{{cookiecutter.project_name}}' "name": "{{cookiecutter.project_name}}",
} },
) )
def update_site_backward(apps, schema_editor): def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default.""" """Revert site domain and name to default."""
Site = apps.get_model('sites', 'Site') Site = apps.get_model("sites", "Site")
Site.objects.update_or_create( Site.objects.update_or_create(
id=settings.SITE_ID, id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
defaults={
'domain': 'example.com',
'name': 'example.com'
}
) )
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("sites", "0002_alter_domain_unique")]
('sites', '0002_alter_domain_unique'),
]
operations = [ operations = [migrations.RunPython(update_site_forward, update_site_backward)]
migrations.RunPython(update_site_forward, update_site_backward),
]

View File

@ -4,10 +4,12 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class AccountAdapter(DefaultAccountAdapter): class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request): def is_open_for_signup(self, request):
return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True) return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
class SocialAccountAdapter(DefaultSocialAccountAdapter): class SocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin): def is_open_for_signup(self, request, sociallogin):
return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True) return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)

View File

@ -6,15 +6,16 @@ from .models import User
class MyUserChangeForm(UserChangeForm): class MyUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta): class Meta(UserChangeForm.Meta):
model = User model = User
class MyUserCreationForm(UserCreationForm): class MyUserCreationForm(UserCreationForm):
error_message = UserCreationForm.error_messages.update({ error_message = UserCreationForm.error_messages.update(
'duplicate_username': 'This username has already been taken.' {"duplicate_username": "This username has already been taken."}
}) )
class Meta(UserCreationForm.Meta): class Meta(UserCreationForm.Meta):
model = User model = User
@ -25,15 +26,14 @@ class MyUserCreationForm(UserCreationForm):
User.objects.get(username=username) User.objects.get(username=username)
except User.DoesNotExist: except User.DoesNotExist:
return username return username
raise forms.ValidationError(self.error_messages['duplicate_username'])
raise forms.ValidationError(self.error_messages["duplicate_username"])
@admin.register(User) @admin.register(User)
class MyUserAdmin(AuthUserAdmin): class MyUserAdmin(AuthUserAdmin):
form = MyUserChangeForm form = MyUserChangeForm
add_form = MyUserCreationForm add_form = MyUserCreationForm
fieldsets = ( fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets
('User Profile', {'fields': ('name',)}), list_display = ("username", "name", "is_superuser")
) + AuthUserAdmin.fieldsets search_fields = ["name"]
list_display = ('username', 'name', 'is_superuser')
search_fields = ['name']

View File

@ -2,7 +2,7 @@ from django.apps import AppConfig
class UsersConfig(AppConfig): class UsersConfig(AppConfig):
name = '{{cookiecutter.project_slug}}.users' name = "{{cookiecutter.project_slug}}.users"
verbose_name = "Users" verbose_name = "Users"
def ready(self): def ready(self):

View File

@ -8,36 +8,125 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [("auth", "0008_alter_user_username_max_length")]
('auth', '0008_alter_user_username_max_length'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('password', models.CharField(max_length=128, verbose_name='password')), "id",
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), models.AutoField(
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), auto_created=True,
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), primary_key=True,
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), serialize=False,
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), verbose_name="ID",
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ("password", models.CharField(max_length=128, verbose_name="password")),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), (
('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')), "last_login",
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), models.DateTimeField(
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"name",
models.CharField(
blank=True, max_length=255, verbose_name="Name of User"
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
], ],
options={ options={
'verbose_name_plural': 'users', "verbose_name_plural": "users",
'verbose_name': 'user', "verbose_name": "user",
'abstract': False, "abstract": False,
}, },
managers=[ managers=[("objects", django.contrib.auth.models.UserManager())],
('objects', django.contrib.auth.models.UserManager()), )
],
),
] ]

View File

@ -8,10 +8,10 @@ class User(AbstractUser):
# First Name and Last Name do not cover name patterns # First Name and Last Name do not cover name patterns
# around the globe. # around the globe.
name = models.CharField(_('Name of User'), blank=True, max_length=255) name = models.CharField(_("Name of User"), blank=True, max_length=255)
def __str__(self): def __str__(self):
return self.username return self.username
def get_absolute_url(self): def get_absolute_url(self):
return reverse('users:detail', kwargs={'username': self.username}) return reverse("users:detail", kwargs={"username": self.username})

View File

@ -2,10 +2,10 @@ import factory
class UserFactory(factory.django.DjangoModelFactory): class UserFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f'user-{n}') username = factory.Sequence(lambda n: f"user-{n}")
email = factory.Sequence(lambda n: f'user-{n}@example.com') email = factory.Sequence(lambda n: f"user-{n}@example.com")
password = factory.PostGenerationMethodCall('set_password', 'password') password = factory.PostGenerationMethodCall("set_password", "password")
class Meta: class Meta:
model = 'users.User' model = "users.User"
django_get_or_create = ('username', ) django_get_or_create = ("username",)

View File

@ -6,30 +6,34 @@ from ..admin import MyUserCreationForm
class TestMyUserCreationForm(TestCase): class TestMyUserCreationForm(TestCase):
def setUp(self): def setUp(self):
self.user = self.make_user('notalamode', 'notalamodespassword') self.user = self.make_user("notalamode", "notalamodespassword")
def test_clean_username_success(self): def test_clean_username_success(self):
# Instantiate the form with a new username # Instantiate the form with a new username
form = MyUserCreationForm({ form = MyUserCreationForm(
'username': 'alamode', {
'password1': '7jefB#f@Cc7YJB]2v', "username": "alamode",
'password2': '7jefB#f@Cc7YJB]2v', "password1": "7jefB#f@Cc7YJB]2v",
}) "password2": "7jefB#f@Cc7YJB]2v",
}
)
# Run is_valid() to trigger the validation # Run is_valid() to trigger the validation
valid = form.is_valid() valid = form.is_valid()
self.assertTrue(valid) self.assertTrue(valid)
# Run the actual clean_username method # Run the actual clean_username method
username = form.clean_username() username = form.clean_username()
self.assertEqual('alamode', username) self.assertEqual("alamode", username)
def test_clean_username_false(self): def test_clean_username_false(self):
# Instantiate the form with the same username as self.user # Instantiate the form with the same username as self.user
form = MyUserCreationForm({ form = MyUserCreationForm(
'username': self.user.username, {
'password1': 'notalamodespassword', "username": self.user.username,
'password2': 'notalamodespassword', "password1": "notalamodespassword",
}) "password2": "notalamodespassword",
}
)
# Run is_valid() to trigger the validation, which is going to fail # Run is_valid() to trigger the validation, which is going to fail
# because the username is already taken # because the username is already taken
valid = form.is_valid() valid = form.is_valid()
@ -37,4 +41,4 @@ class TestMyUserCreationForm(TestCase):
# The form.errors dict should contain a single error called 'username' # The form.errors dict should contain a single error called 'username'
self.assertTrue(len(form.errors) == 1) self.assertTrue(len(form.errors) == 1)
self.assertTrue('username' in form.errors) self.assertTrue("username" in form.errors)

View File

@ -9,11 +9,8 @@ class TestUser(TestCase):
def test__str__(self): def test__str__(self):
self.assertEqual( self.assertEqual(
self.user.__str__(), self.user.__str__(),
'testuser' # This is the default username for self.make_user() "testuser", # This is the default username for self.make_user()
) )
def test_get_absolute_url(self): def test_get_absolute_url(self):
self.assertEqual( self.assertEqual(self.user.get_absolute_url(), "/users/testuser/")
self.user.get_absolute_url(),
'/users/testuser/'
)

View File

@ -11,41 +11,34 @@ class TestUserURLs(TestCase):
def test_list_reverse(self): def test_list_reverse(self):
"""users:list should reverse to /users/.""" """users:list should reverse to /users/."""
self.assertEqual(reverse('users:list'), '/users/') self.assertEqual(reverse("users:list"), "/users/")
def test_list_resolve(self): def test_list_resolve(self):
"""/users/ should resolve to users:list.""" """/users/ should resolve to users:list."""
self.assertEqual(resolve('/users/').view_name, 'users:list') self.assertEqual(resolve("/users/").view_name, "users:list")
def test_redirect_reverse(self): def test_redirect_reverse(self):
"""users:redirect should reverse to /users/~redirect/.""" """users:redirect should reverse to /users/~redirect/."""
self.assertEqual(reverse('users:redirect'), '/users/~redirect/') self.assertEqual(reverse("users:redirect"), "/users/~redirect/")
def test_redirect_resolve(self): def test_redirect_resolve(self):
"""/users/~redirect/ should resolve to users:redirect.""" """/users/~redirect/ should resolve to users:redirect."""
self.assertEqual( self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect")
resolve('/users/~redirect/').view_name,
'users:redirect'
)
def test_detail_reverse(self): def test_detail_reverse(self):
"""users:detail should reverse to /users/testuser/.""" """users:detail should reverse to /users/testuser/."""
self.assertEqual( self.assertEqual(
reverse('users:detail', kwargs={'username': 'testuser'}), reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/"
'/users/testuser/'
) )
def test_detail_resolve(self): def test_detail_resolve(self):
"""/users/testuser/ should resolve to users:detail.""" """/users/testuser/ should resolve to users:detail."""
self.assertEqual(resolve('/users/testuser/').view_name, 'users:detail') self.assertEqual(resolve("/users/testuser/").view_name, "users:detail")
def test_update_reverse(self): def test_update_reverse(self):
"""users:update should reverse to /users/~update/.""" """users:update should reverse to /users/~update/."""
self.assertEqual(reverse('users:update'), '/users/~update/') self.assertEqual(reverse("users:update"), "/users/~update/")
def test_update_resolve(self): def test_update_resolve(self):
"""/users/~update/ should resolve to users:update.""" """/users/~update/ should resolve to users:update."""
self.assertEqual( self.assertEqual(resolve("/users/~update/").view_name, "users:update")
resolve('/users/~update/').view_name,
'users:update'
)

View File

@ -2,10 +2,7 @@ from django.test import RequestFactory
from test_plus.test import TestCase from test_plus.test import TestCase
from ..views import ( from ..views import (UserRedirectView, UserUpdateView)
UserRedirectView,
UserUpdateView
)
class BaseUserTestCase(TestCase): class BaseUserTestCase(TestCase):
@ -21,17 +18,14 @@ class TestUserRedirectView(BaseUserTestCase):
# Instantiate the view directly. Never do this outside a test! # Instantiate the view directly. Never do this outside a test!
view = UserRedirectView() view = UserRedirectView()
# Generate a fake request # Generate a fake request
request = self.factory.get('/fake-url') request = self.factory.get("/fake-url")
# Attach the user to the request # Attach the user to the request
request.user = self.user request.user = self.user
# Attach the request to the view # Attach the request to the view
view.request = request view.request = request
# Expect: '/users/testuser/', as that is the default username for # Expect: '/users/testuser/', as that is the default username for
# self.make_user() # self.make_user()
self.assertEqual( self.assertEqual(view.get_redirect_url(), "/users/testuser/")
view.get_redirect_url(),
'/users/testuser/'
)
class TestUserUpdateView(BaseUserTestCase): class TestUserUpdateView(BaseUserTestCase):
@ -42,7 +36,7 @@ class TestUserUpdateView(BaseUserTestCase):
# Instantiate the view directly. Never do this outside a test! # Instantiate the view directly. Never do this outside a test!
self.view = UserUpdateView() self.view = UserUpdateView()
# Generate a fake request # Generate a fake request
request = self.factory.get('/fake-url') request = self.factory.get("/fake-url")
# Attach the user to the request # Attach the user to the request
request.user = self.user request.user = self.user
# Attach the request to the view # Attach the request to the view
@ -51,14 +45,8 @@ class TestUserUpdateView(BaseUserTestCase):
def test_get_success_url(self): def test_get_success_url(self):
# Expect: '/users/testuser/', as that is the default username for # Expect: '/users/testuser/', as that is the default username for
# self.make_user() # self.make_user()
self.assertEqual( self.assertEqual(self.view.get_success_url(), "/users/testuser/")
self.view.get_success_url(),
'/users/testuser/'
)
def test_get_object(self): def test_get_object(self):
# Expect: self.user, as that is the request's user object # Expect: self.user, as that is the request's user object
self.assertEqual( self.assertEqual(self.view.get_object(), self.user)
self.view.get_object(),
self.user
)

View File

@ -2,26 +2,14 @@ from django.conf.urls import url
from . import views from . import views
app_name = 'users' app_name = "users"
urlpatterns = [ urlpatterns = [
url(regex=r"^$", view=views.UserListView.as_view(), name="list"),
url(regex=r"^~redirect/$", view=views.UserRedirectView.as_view(), name="redirect"),
url(regex=r"^~update/$", view=views.UserUpdateView.as_view(), name="update"),
url( url(
regex=r'^$', regex=r"^(?P<username>[\w.@+-]+)/$",
view=views.UserListView.as_view(),
name='list'
),
url(
regex=r'^~redirect/$',
view=views.UserRedirectView.as_view(),
name='redirect'
),
url(
regex=r'^~update/$',
view=views.UserUpdateView.as_view(),
name='update'
),
url(
regex=r'^(?P<username>[\w.@+-]+)/$',
view=views.UserDetailView.as_view(), view=views.UserDetailView.as_view(),
name='detail' name="detail",
), ),
] ]

View File

@ -8,29 +8,28 @@ from .models import User
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
model = User model = User
# These next two lines tell the view to index lookups by username # These next two lines tell the view to index lookups by username
slug_field = 'username' slug_field = "username"
slug_url_kwarg = 'username' slug_url_kwarg = "username"
class UserRedirectView(LoginRequiredMixin, RedirectView): class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False permanent = False
def get_redirect_url(self): def get_redirect_url(self):
return reverse('users:detail', return reverse("users:detail", kwargs={"username": self.request.user.username})
kwargs={'username': self.request.user.username})
class UserUpdateView(LoginRequiredMixin, UpdateView): class UserUpdateView(LoginRequiredMixin, UpdateView):
fields = ['name', ] fields = ["name"]
# we already imported User in the view code above, remember? # we already imported User in the view code above, remember?
model = User model = User
# send the user back to their own page after a successful update # send the user back to their own page after a successful update
def get_success_url(self): def get_success_url(self):
return reverse('users:detail', return reverse("users:detail", kwargs={"username": self.request.user.username})
kwargs={'username': self.request.user.username})
def get_object(self): def get_object(self):
# Only get the User record for the user making the request # Only get the User record for the user making the request
@ -40,5 +39,5 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
class UserListView(LoginRequiredMixin, ListView): class UserListView(LoginRequiredMixin, ListView):
model = User model = User
# These next two lines tell the view to index lookups by username # These next two lines tell the view to index lookups by username
slug_field = 'username' slug_field = "username"
slug_url_kwarg = 'username' slug_url_kwarg = "username"