summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruno Alla <alla.brunoo@gmail.com>2025-04-27 12:01:24 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-04-27 14:41:27 +0100
commitbc21bc4282d854053c1218e8fb3ea39387ccca72 (patch)
tree548330c1779ad6049aa8389c6456b628c657e276
parent3babda775d822232c8438dba8206cd2f3e2dfacc (diff)
Fixed #18296 -- Created missing custom target directory for startproject and startapp.
-rw-r--r--django/core/management/templates.py12
-rw-r--r--docs/intro/tutorial01.txt15
-rw-r--r--docs/ref/django-admin.txt21
-rw-r--r--docs/releases/6.0.txt3
-rw-r--r--tests/admin_scripts/tests.py71
5 files changed, 92 insertions, 30 deletions
diff --git a/django/core/management/templates.py b/django/core/management/templates.py
index dbaea11200..ea2c4a294f 100644
--- a/django/core/management/templates.py
+++ b/django/core/management/templates.py
@@ -46,7 +46,9 @@ class TemplateCommand(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("name", help="Name of the application or project.")
parser.add_argument(
- "directory", nargs="?", help="Optional destination directory"
+ "directory",
+ nargs="?",
+ help="Optional destination directory, this will be created if needed.",
)
parser.add_argument(
"--template", help="The path or URL to load the template from."
@@ -105,10 +107,10 @@ class TemplateCommand(BaseCommand):
if app_or_project == "app":
self.validate_name(os.path.basename(top_dir), "directory")
if not os.path.exists(top_dir):
- raise CommandError(
- "Destination directory '%s' does not "
- "exist, please create it first." % top_dir
- )
+ try:
+ os.makedirs(top_dir)
+ except OSError as e:
+ raise CommandError(e)
# Find formatters, which are external executables, before input
# from the templates can sneak into the path.
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index 0a766f9099..1318c5aa08 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -45,21 +45,16 @@ including database configuration, Django-specific options and
application-specific settings.
From the command line, ``cd`` into a directory where you'd like to store your
-code and create a new directory named ``djangotutorial``. (This directory name
-doesn't matter to Django; you can rename it to anything you like.)
-
-.. console::
-
- $ mkdir djangotutorial
-
-Then, run the following command to bootstrap a new Django project:
+code and run the following command to bootstrap a new Django project:
.. console::
$ django-admin startproject mysite djangotutorial
-This will create a project called ``mysite`` inside the ``djangotutorial``
-directory. If it didn't work, see :ref:`troubleshooting-django-admin`.
+This will create a directory ``djangotutorial`` with a project called
+``mysite`` inside. The directory name doesn't matter to Django; you can rename
+it to anything you like. If it didn't work, see
+:ref:`troubleshooting-django-admin`.
.. note::
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 74413b78f9..a1603c9e84 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -1266,9 +1266,13 @@ By default, :source:`the new directory <django/conf/app_template>` contains a
``models.py`` file and other app template files. If only the app name is given,
the app directory will be created in the current working directory.
-If the optional destination is provided, Django will use that existing
-directory rather than creating a new one. You can use '.' to denote the current
-working directory.
+If the optional destination is provided, Django will use that name instead. If
+the directory with the given name doesn't exist, it will be created. You can
+use '.' to denote the current working directory.
+
+.. versionchanged:: 6.0
+
+ Automatic creation of the destination directory was added.
For example:
@@ -1378,9 +1382,14 @@ If only the project name is given, both the project directory and project
package will be named ``<projectname>`` and the project directory
will be created in the current working directory.
-If the optional destination is provided, Django will use that existing
-directory as the project directory, and create ``manage.py`` and the project
-package within it. Use '.' to denote the current working directory.
+If the optional destination is provided, Django will use that name as the
+project directory, and create ``manage.py`` and the project package within it.
+If the directory with the given name doesn't exist, it will be created. Use '.'
+to denote the current working directory.
+
+.. versionchanged:: 6.0
+
+ Automatic creation of the destination directory was added.
For example:
diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt
index 4b9ad92e92..341da6fd68 100644
--- a/docs/releases/6.0.txt
+++ b/docs/releases/6.0.txt
@@ -170,7 +170,8 @@ Logging
Management Commands
~~~~~~~~~~~~~~~~~~~
-* ...
+* The :djadmin:`startproject` and :djadmin:`startapp` commands now create the
+ custom target directory if it doesn't exist.
Migrations
~~~~~~~~~~
diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py
index e4900eefd5..ebdb637df4 100644
--- a/tests/admin_scripts/tests.py
+++ b/tests/admin_scripts/tests.py
@@ -2843,8 +2843,7 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
def test_custom_project_destination_missing(self):
"""
- Make sure an exception is raised when the provided
- destination directory doesn't exist
+ Create the directory when the provided destination directory doesn't exist.
"""
template_path = os.path.join(custom_templates_dir, "project_template")
args = [
@@ -2857,12 +2856,8 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
testproject_dir = os.path.join(self.test_dir, "project_dir2")
out, err = self.run_django_admin(args)
self.assertNoOutput(out)
- self.assertOutput(
- err,
- "Destination directory '%s' does not exist, please create it first."
- % testproject_dir,
- )
- self.assertFalse(os.path.exists(testproject_dir))
+ self.assertNoOutput(err)
+ self.assertTrue(os.path.exists(testproject_dir))
def test_custom_project_template_with_non_ascii_templates(self):
"""
@@ -3099,6 +3094,66 @@ class StartApp(AdminScriptTestCase):
content,
)
+ def test_creates_directory_when_custom_app_destination_missing(self):
+ args = [
+ "startapp",
+ "my_app",
+ "my_app",
+ ]
+ testapp_dir = os.path.join(self.test_dir, "my_app")
+ out, err = self.run_django_admin(args)
+ self.assertNoOutput(out)
+ self.assertNoOutput(err)
+ self.assertTrue(os.path.exists(testapp_dir))
+
+ def test_custom_app_destination_missing_with_nested_subdirectory(self):
+ args = [
+ "startapp",
+ "my_app",
+ "apps/my_app",
+ ]
+ testapp_dir = os.path.join(self.test_dir, "apps", "my_app")
+ out, err = self.run_django_admin(args)
+ self.assertNoOutput(out)
+ self.assertNoOutput(err)
+ self.assertTrue(os.path.exists(testapp_dir))
+
+ def test_custom_name_with_app_within_other_app(self):
+ parent_app_dir = os.path.join(self.test_dir, "parent")
+ self.run_django_admin(["startapp", "parent"])
+ self.assertTrue(os.path.exists(parent_app_dir))
+
+ nested_args = ["startapp", "child", "parent/child"]
+ child_app_dir = os.path.join(self.test_dir, "parent", "child")
+ out, err = self.run_django_admin(nested_args)
+ self.assertNoOutput(out)
+ self.assertNoOutput(err)
+ self.assertTrue(os.path.exists(child_app_dir))
+
+ @unittest.skipIf(
+ sys.platform == "win32",
+ "Windows only partially supports umasks and chmod.",
+ )
+ def test_custom_app_directory_creation_error_handling(self):
+ """The error is displayed to the user in case of OSError."""
+ args = [
+ "startapp",
+ "my_app",
+ "project_dir/my_app",
+ ]
+ # Create a read-only parent directory.
+ os.makedirs(
+ os.path.join(self.test_dir, "project_dir"), exist_ok=True, mode=0o200
+ )
+ testapp_dir = os.path.join(self.test_dir, "project_dir", "my_app")
+ out, err = self.run_django_admin(args)
+ self.assertNoOutput(out)
+ self.assertOutput(
+ err,
+ "Permission denied",
+ )
+ self.assertFalse(os.path.exists(testapp_dir))
+
class DiffSettings(AdminScriptTestCase):
"""Tests for diffsettings management command."""