From bc21bc4282d854053c1218e8fb3ea39387ccca72 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sun, 27 Apr 2025 12:01:24 +0200 Subject: [PATCH] Fixed #18296 -- Created missing custom target directory for startproject and startapp. --- django/core/management/templates.py | 12 +++-- docs/intro/tutorial01.txt | 15 ++---- docs/ref/django-admin.txt | 21 ++++++--- docs/releases/6.0.txt | 3 +- tests/admin_scripts/tests.py | 71 +++++++++++++++++++++++++---- 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 ` 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 ```` 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."""