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 = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = 'Cookiecutter Django'
project = "Cookiecutter Django"
copyright = "2013-2018, Daniel Roy Greenfeld".format(now.year)
# 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.
#
# The short X.Y version.
version = '{}.{}.{}'.format(*now.isocalendar())
version = "{}.{}.{}".format(*now.isocalendar())
# 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
# 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
# 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.
# default_role = None
@ -82,7 +82,7 @@ exclude_patterns = ['_build']
# show_authors = False
# 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.
# modindex_common_prefix = []
@ -92,7 +92,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# 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
# 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,
# relative to this directory. They are copied after the builtin static files,
# 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,
# using the given strftime format.
@ -165,7 +165,7 @@ html_static_path = ['_static']
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'cookiecutter-djangodoc'
htmlhelp_basename = "cookiecutter-djangodoc"
# -- Options for LaTeX output --------------------------------------------------
@ -173,10 +173,8 @@ htmlhelp_basename = 'cookiecutter-djangodoc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@ -184,10 +182,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index',
'cookiecutter-django.tex',
'cookiecutter-django Documentation',
'cookiecutter-django', 'manual'),
(
"index",
"cookiecutter-django.tex",
"cookiecutter-django Documentation",
"cookiecutter-django",
"manual",
)
]
# 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
# (source start file, name, description, authors, manual section).
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.
@ -230,9 +236,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Cookiecutter Django', 'Cookiecutter Django documentation',
'Daniel Roy Greenfeld', 'Cookiecutter Django',
'A Cookiecutter template for creating production-ready Django projects quickly.', 'Miscellaneous'),
(
"index",
"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.

View File

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

View File

@ -16,40 +16,41 @@ INFO = "\x1b[1;33m [INFO]: "
HINT = "\x1b[3;33m"
SUCCESS = "\x1b[1;32m [SUCCESS]: "
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)
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
)
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]
if python_major_version == 2:
print(
WARNING +
"Cookiecutter Django does not support Python 2. "
WARNING + "Cookiecutter Django does not support Python 2. "
"Stability is guaranteed with Python 3.6+ only, "
"are you sure you want to proceed (y/n)? " +
TERMINATOR
"are you sure you want to proceed (y/n)? " + TERMINATOR
)
yes_options, no_options = frozenset(['y']), frozenset(['n'])
yes_options, no_options = frozenset(["y"]), frozenset(["n"])
while True:
choice = raw_input().lower()
if choice in yes_options:
break
elif choice in no_options:
print(
INFO +
"Generation process stopped as requested." +
TERMINATOR
)
print(INFO + "Generation process stopped as requested." + TERMINATOR)
sys.exit(1)
else:
print(
HINT +
"Please respond with {} or {}: ".format(
', '.join(["'{}'".format(o) for o in yes_options if not o == '']),
', '.join(["'{}'".format(o) for o in no_options if not o == ''])
) +
TERMINATOR
HINT
+ "Please respond with {} or {}: ".format(
", ".join(
["'{}'".format(o) for o in yes_options if not o == ""]
),
", ".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
# 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 push --tags')
os.system("git push --tags")
sys.exit()
with open('README.rst') as readme_file:
with open("README.rst") as readme_file:
long_description = readme_file.read()
setup(
name='cookiecutter-django',
name="cookiecutter-django",
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,
author='Daniel Roy Greenfeld',
author_email='pydanny@gmail.com',
url='https://github.com/pydanny/cookiecutter-django',
author="Daniel Roy Greenfeld",
author_email="pydanny@gmail.com",
url="https://github.com/pydanny/cookiecutter-django",
packages=[],
license='BSD',
license="BSD",
zip_safe=False,
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Framework :: Django :: 2.0',
'Intended Audience :: Developers',
'Natural Language :: English',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Software Development',
"Development Status :: 4 - Beta",
"Environment :: Console",
"Framework :: Django :: 2.0",
"Intended Audience :: Developers",
"Natural Language :: English",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development",
],
keywords=(
'cookiecutter, Python, projects, project templates, django, '
'skeleton, scaffolding, project directory, setup.py'
"cookiecutter, Python, projects, project templates, django, "
"skeleton, scaffolding, project directory, setup.py"
),
)

View File

@ -5,21 +5,21 @@ import sh
import pytest
from binaryornot.check import is_binary
PATTERN = '{{(\s?cookiecutter)[.](.*?)}}'
PATTERN = "{{(\s?cookiecutter)[.](.*?)}}"
RE_OBJ = re.compile(PATTERN)
@pytest.fixture
def context():
return {
'project_name': 'My Test Project',
'project_slug': 'my_test_project',
'author_name': 'Test Author',
'email': 'test@example.com',
'description': 'A short description of the project.',
'domain_name': 'example.com',
'version': '0.1.0',
'timezone': 'UTC',
"project_name": "My Test Project",
"project_slug": "my_test_project",
"author_name": "Test Author",
"email": "test@example.com",
"description": "A short description of the project.",
"domain_name": "example.com",
"version": "0.1.0",
"timezone": "UTC",
}
@ -40,9 +40,10 @@ def check_paths(paths):
for path in paths:
if is_binary(path):
continue
for line in open(path, 'r'):
for line in open(path, "r"):
match = RE_OBJ.search(line)
msg = 'cookiecutter variable not replaced in {}'
msg = "cookiecutter variable not replaced in {}"
assert match is None, msg.format(path)
@ -50,7 +51,7 @@ def test_default_configuration(cookies, context):
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
assert result.exception is None
assert result.project.basename == context['project_slug']
assert result.project.basename == context["project_slug"]
assert result.project.isdir()
paths = build_files_list(str(result.project))
@ -58,9 +59,9 @@ def test_default_configuration(cookies, context):
check_paths(paths)
@pytest.fixture(params=['use_mailhog', 'use_celery', 'windows'])
@pytest.fixture(params=["use_mailhog", "use_celery", "windows"])
def feature_context(request, context):
context.update({request.param: 'y'})
context.update({request.param: "y"})
return context
@ -68,7 +69,7 @@ def test_enabled_features(cookies, feature_context):
result = cookies.bake(extra_context=feature_context)
assert result.exit_code == 0
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()
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
DEBUG = False
# 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
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
TEST_RUNNER = "django.test.runner.DiscoverRunner"
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': ''
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": ""
}
}
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405
TEMPLATES[0]['OPTIONS']['loaders'] = [ # noqa F405
TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # 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.app_directories.Loader',
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
),
)
]
# EMAIL
# ------------------------------------------------------------------------------
# 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
EMAIL_HOST = 'localhost'
EMAIL_HOST = "localhost"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025

View File

@ -6,32 +6,47 @@ from django.views.generic import TemplateView
from django.views import defaults as default_views
urlpatterns = [
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"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"),
url(
r"^about/$",
TemplateView.as_view(template_name="pages/about.html"),
name="about",
),
# Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %}
url(settings.ADMIN_URL, admin.site.urls),
# User management
url(r'^users/', include('{{ cookiecutter.project_slug }}.users.urls', namespace='users')),
url(r'^accounts/', include('allauth.urls')),
url(
r"^users/",
include("{{ cookiecutter.project_slug }}.users.urls", namespace="users"),
),
url(r"^accounts/", include("allauth.urls")),
# 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:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
urlpatterns += [
url(r'^400/$', default_views.bad_request, 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),
url(
r"^400/$",
default_views.bad_request,
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
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns

View File

@ -27,19 +27,19 @@ import sys
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = '{{ cookiecutter.project_name }}'
project = "{{ cookiecutter.project_name }}"
copyright = """{% now 'utc', '%Y' %}, {{ cookiecutter.author_name }}"""
# 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.
#
# The short X.Y version.
version = '0.1'
version = "0.1"
# The full version, including alpha/beta/rc tags.
release = '0.1'
release = "0.1"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -63,7 +63,7 @@ release = '0.1'
# List of patterns, relative to source directory, that match files and
# 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.
# default_role = None
@ -80,7 +80,7 @@ exclude_patterns = ['_build']
# show_authors = False
# 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.
# modindex_common_prefix = []
@ -90,7 +90,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# 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
# 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,
# relative to this directory. They are copied after the builtin static files,
# 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,
# using the given strftime format.
@ -163,7 +163,7 @@ html_static_path = ['_static']
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = '{{ cookiecutter.project_slug }}doc'
htmlhelp_basename = "{{ cookiecutter.project_slug }}doc"
# -- Options for LaTeX output --------------------------------------------------
@ -171,10 +171,8 @@ htmlhelp_basename = '{{ cookiecutter.project_slug }}doc'
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@ -182,10 +180,13 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index',
'{{ cookiecutter.project_slug }}.tex',
'{{ cookiecutter.project_name }} Documentation',
"""{{ cookiecutter.author_name }}""", 'manual'),
(
"index",
"{{ cookiecutter.project_slug }}.tex",
"{{ cookiecutter.project_name }} Documentation",
"""{{ cookiecutter.author_name }}""",
"manual",
)
]
# 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
# (source start file, name, description, authors, manual section).
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.
@ -228,9 +234,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', '{{ cookiecutter.project_slug }}', '{{ cookiecutter.project_name }} Documentation',
"""{{ cookiecutter.author_name }}""", '{{ cookiecutter.project_name }}',
"""{{ cookiecutter.description }}""", 'Miscellaneous'),
(
"index",
"{{ 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.

View File

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

View File

@ -4,21 +4,21 @@ from typing import Sequence
import pytest
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 = [
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.django'),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.postgres'),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, '.caddy'),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"),
os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"),
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,
merged_file_paths: Sequence[str],
append_linesep: bool = True) -> None:
with open(output_file_path, 'w') as output_file:
def merge(
output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True
) -> None:
with open(output_file_path, "w") as output_file:
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()
output_file.write(merged_file_content)
if append_linesep:
@ -29,26 +29,24 @@ def main():
merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS)
@pytest.mark.parametrize('merged_file_count', range(3))
@pytest.mark.parametrize('append_linesep', [True, False])
def test_merge(tmpdir_factory,
merged_file_count: int,
append_linesep: bool):
@pytest.mark.parametrize("merged_file_count", range(3))
@pytest.mark.parametrize("append_linesep", [True, False])
def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
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 = []
for i in range(merged_file_count):
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_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)
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)
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()
assert actual_output_file_content == expected_output_file_content
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -1,2 +1,7 @@
__version__ = '{{ cookiecutter.version }}'
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
__version__ = "{{ cookiecutter.version }}"
__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 = [
migrations.CreateModel(
name='Site',
name="Site",
fields=[
('id', models.AutoField(verbose_name='ID', 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')),
(
"id",
models.AutoField(
verbose_name="ID",
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={
'ordering': ('domain',),
'db_table': 'django_site',
'verbose_name': 'site',
'verbose_name_plural': 'sites',
"ordering": ("domain",),
"db_table": "django_site",
"verbose_name": "site",
"verbose_name_plural": "sites",
},
bases=(models.Model,),
managers=[
('objects', django.contrib.sites.models.SiteManager()),
],
),
managers=[("objects", django.contrib.sites.models.SiteManager())],
)
]

View File

@ -4,17 +4,17 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sites', '0001_initial'),
]
dependencies = [("sites", "0001_initial")]
operations = [
migrations.AlterField(
model_name='site',
name='domain',
model_name="site",
name="domain",
field=models.CharField(
max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator],
verbose_name='domain name'
max_length=100,
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):
"""Set site domain and name."""
Site = apps.get_model('sites', 'Site')
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
'domain': '{{cookiecutter.domain_name}}',
'name': '{{cookiecutter.project_name}}'
}
"domain": "{{cookiecutter.domain_name}}",
"name": "{{cookiecutter.project_name}}",
},
)
def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default."""
Site = apps.get_model('sites', 'Site')
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
'domain': 'example.com',
'name': 'example.com'
}
id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
)
class Migration(migrations.Migration):
dependencies = [
('sites', '0002_alter_domain_unique'),
]
dependencies = [("sites", "0002_alter_domain_unique")]
operations = [
migrations.RunPython(update_site_forward, update_site_backward),
]
operations = [migrations.RunPython(update_site_forward, update_site_backward)]

View File

@ -4,10 +4,12 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True)
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
class SocialAccountAdapter(DefaultSocialAccountAdapter):
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 Meta(UserChangeForm.Meta):
model = User
class MyUserCreationForm(UserCreationForm):
error_message = UserCreationForm.error_messages.update({
'duplicate_username': 'This username has already been taken.'
})
error_message = UserCreationForm.error_messages.update(
{"duplicate_username": "This username has already been taken."}
)
class Meta(UserCreationForm.Meta):
model = User
@ -25,15 +26,14 @@ class MyUserCreationForm(UserCreationForm):
User.objects.get(username=username)
except User.DoesNotExist:
return username
raise forms.ValidationError(self.error_messages['duplicate_username'])
raise forms.ValidationError(self.error_messages["duplicate_username"])
@admin.register(User)
class MyUserAdmin(AuthUserAdmin):
form = MyUserChangeForm
add_form = MyUserCreationForm
fieldsets = (
('User Profile', {'fields': ('name',)}),
) + AuthUserAdmin.fieldsets
list_display = ('username', 'name', 'is_superuser')
search_fields = ['name']
fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets
list_display = ("username", "name", "is_superuser")
search_fields = ["name"]

View File

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

View File

@ -8,36 +8,125 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
dependencies = [("auth", "0008_alter_user_username_max_length")]
operations = [
migrations.CreateModel(
name='User',
name="User",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(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')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
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={
'verbose_name_plural': 'users',
'verbose_name': 'user',
'abstract': False,
"verbose_name_plural": "users",
"verbose_name": "user",
"abstract": False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
managers=[("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
# 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):
return self.username
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):
username = factory.Sequence(lambda n: f'user-{n}')
email = factory.Sequence(lambda n: f'user-{n}@example.com')
password = factory.PostGenerationMethodCall('set_password', 'password')
username = factory.Sequence(lambda n: f"user-{n}")
email = factory.Sequence(lambda n: f"user-{n}@example.com")
password = factory.PostGenerationMethodCall("set_password", "password")
class Meta:
model = 'users.User'
django_get_or_create = ('username', )
model = "users.User"
django_get_or_create = ("username",)

View File

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

View File

@ -11,41 +11,34 @@ class TestUserURLs(TestCase):
def test_list_reverse(self):
"""users:list should reverse to /users/."""
self.assertEqual(reverse('users:list'), '/users/')
self.assertEqual(reverse("users:list"), "/users/")
def test_list_resolve(self):
"""/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):
"""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):
"""/users/~redirect/ should resolve to users:redirect."""
self.assertEqual(
resolve('/users/~redirect/').view_name,
'users:redirect'
)
self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect")
def test_detail_reverse(self):
"""users:detail should reverse to /users/testuser/."""
self.assertEqual(
reverse('users:detail', kwargs={'username': 'testuser'}),
'/users/testuser/'
reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/"
)
def test_detail_resolve(self):
"""/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):
"""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):
"""/users/~update/ should resolve to users:update."""
self.assertEqual(
resolve('/users/~update/').view_name,
'users:update'
)
self.assertEqual(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 ..views import (
UserRedirectView,
UserUpdateView
)
from ..views import (UserRedirectView, UserUpdateView)
class BaseUserTestCase(TestCase):
@ -21,17 +18,14 @@ class TestUserRedirectView(BaseUserTestCase):
# Instantiate the view directly. Never do this outside a test!
view = UserRedirectView()
# Generate a fake request
request = self.factory.get('/fake-url')
request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
view.request = request
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
self.assertEqual(
view.get_redirect_url(),
'/users/testuser/'
)
self.assertEqual(view.get_redirect_url(), "/users/testuser/")
class TestUserUpdateView(BaseUserTestCase):
@ -42,7 +36,7 @@ class TestUserUpdateView(BaseUserTestCase):
# Instantiate the view directly. Never do this outside a test!
self.view = UserUpdateView()
# Generate a fake request
request = self.factory.get('/fake-url')
request = self.factory.get("/fake-url")
# Attach the user to the request
request.user = self.user
# Attach the request to the view
@ -51,14 +45,8 @@ class TestUserUpdateView(BaseUserTestCase):
def test_get_success_url(self):
# Expect: '/users/testuser/', as that is the default username for
# self.make_user()
self.assertEqual(
self.view.get_success_url(),
'/users/testuser/'
)
self.assertEqual(self.view.get_success_url(), "/users/testuser/")
def test_get_object(self):
# Expect: self.user, as that is the request's user object
self.assertEqual(
self.view.get_object(),
self.user
)
self.assertEqual(self.view.get_object(), self.user)

View File

@ -2,26 +2,14 @@ from django.conf.urls import url
from . import views
app_name = 'users'
app_name = "users"
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(
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(
regex=r'^(?P<username>[\w.@+-]+)/$',
regex=r"^(?P<username>[\w.@+-]+)/$",
view=views.UserDetailView.as_view(),
name='detail'
name="detail",
),
]

View File

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