Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion django/core/management/templates.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import cgi
import mimetypes
import os
Expand Down Expand Up @@ -54,6 +55,14 @@ def add_arguments(self, parser):
help='The file name(s) to render. Separate multiple file names '
'with commas, or use -n multiple times.'
)
parser.add_argument(
'--exclude', '-x',
action='append', default=argparse.SUPPRESS, nargs='?', const='',
help=(
'The directory name(s) to exclude, in addition to .git and '
'__pycache__. Can be used multiple times.'
),
)

def handle(self, app_or_project, name, target=None, **options):
self.app_or_project = app_or_project
Expand Down Expand Up @@ -82,8 +91,12 @@ def handle(self, app_or_project, name, target=None, **options):

extensions = tuple(handle_extensions(options['extensions']))
extra_files = []
excluded_directories = ['.git', '__pycache__']
for file in options['files']:
extra_files.extend(map(lambda x: x.strip(), file.split(',')))
if exclude := options.get('exclude'):
for directory in exclude:
excluded_directories.append(directory.strip())
if self.verbosity >= 2:
self.stdout.write(
'Rendering %s template files with extensions: %s'
Expand Down Expand Up @@ -126,7 +139,10 @@ def handle(self, app_or_project, name, target=None, **options):
os.makedirs(target_dir, exist_ok=True)

for dirname in dirs[:]:
if dirname.startswith('.') or dirname == '__pycache__':
if 'exclude' not in options:
if dirname.startswith('.') or dirname == '__pycache__':
dirs.remove(dirname)
elif dirname in excluded_directories:
dirs.remove(dirname)

for filename in files:
Expand Down
16 changes: 16 additions & 0 deletions docs/ref/django-admin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,14 @@ Specifies which files in the app template (in addition to those matching
``--extension``) should be rendered with the template engine. Defaults to an
empty list.

.. django-admin-option:: --exclude DIRECTORIES, -x DIRECTORIES

.. versionadded:: 4.0

Specifies which directories in the app template should be excluded, in addition
to ``.git`` and ``__pycache__``. If this option is not provided, directories
named ``__pycache__`` or starting with ``.`` will be excluded.

The :class:`template context <django.template.Context>` used for all matching
files is:

Expand Down Expand Up @@ -1373,6 +1381,14 @@ Specifies which files in the project template (in addition to those matching
``--extension``) should be rendered with the template engine. Defaults to an
empty list.

.. django-admin-option:: --exclude DIRECTORIES, -x DIRECTORIES

.. versionadded:: 4.0

Specifies which directories in the project template should be excluded, in
addition to ``.git`` and ``__pycache__``. If this option is not provided,
directories named ``__pycache__`` or starting with ``.`` will be excluded.

The :class:`template context <django.template.Context>` used is:

- Any option passed to the ``startproject`` command (among the command's
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/4.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ Management Commands
<django.core.management.BaseCommand.suppressed_base_arguments>` attribute
allows suppressing unsupported default command options in the help output.

* The new :option:`startapp --exclude` and :option:`startproject --exclude`
options allow excluding directories from the template.

Migrations
~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# The {{ project_name }} should be rendered.
82 changes: 82 additions & 0 deletions tests/admin_scripts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,88 @@ def test_custom_project_template_with_non_ascii_templates(self):
'Some non-ASCII text for testing ticket #18091:',
'üäö €'])

def test_custom_project_template_hidden_directory_default_excluded(self):
"""Hidden directories are excluded by default."""
template_path = os.path.join(custom_templates_dir, 'project_template')
args = [
'startproject',
'--template',
template_path,
'custom_project_template_hidden_directories',
'project_dir',
]
testproject_dir = os.path.join(self.test_dir, 'project_dir')
os.mkdir(testproject_dir)

_, err = self.run_django_admin(args)
self.assertNoOutput(err)
hidden_dir = os.path.join(testproject_dir, '.hidden')
self.assertIs(os.path.exists(hidden_dir), False)

def test_custom_project_template_hidden_directory_included(self):
"""
Template context variables in hidden directories are rendered, if not
excluded.
"""
template_path = os.path.join(custom_templates_dir, 'project_template')
project_name = 'custom_project_template_hidden_directories_included'
args = [
'startproject',
'--template',
template_path,
project_name,
'project_dir',
'--exclude',
]
testproject_dir = os.path.join(self.test_dir, 'project_dir')
os.mkdir(testproject_dir)

_, err = self.run_django_admin(args)
self.assertNoOutput(err)
render_py_path = os.path.join(testproject_dir, '.hidden', 'render.py')
with open(render_py_path) as fp:
self.assertIn(
f'# The {project_name} should be rendered.',
fp.read(),
)

def test_custom_project_template_exclude_directory(self):
"""
Excluded directories (in addition to .git and __pycache__) are not
included in the project.
"""
template_path = os.path.join(custom_templates_dir, 'project_template')
project_name = 'custom_project_with_excluded_directories'
args = [
'startproject',
'--template',
template_path,
project_name,
'project_dir',
'--exclude',
'additional_dir',
'-x',
'.hidden',
]
testproject_dir = os.path.join(self.test_dir, 'project_dir')
os.mkdir(testproject_dir)

_, err = self.run_django_admin(args)
self.assertNoOutput(err)
excluded_directories = [
'.hidden',
'additional_dir',
'.git',
'__pycache__',
]
for directory in excluded_directories:
self.assertIs(
os.path.exists(os.path.join(testproject_dir, directory)),
False,
)
not_excluded = os.path.join(testproject_dir, project_name)
self.assertIs(os.path.exists(not_excluded), True)


class StartApp(AdminScriptTestCase):

Expand Down