Permalink
Browse files

Fixed #17042 -- Extended startproject and startapp management command…

…s to better handle custom app and project templates. Many thanks to Preston Holmes for his initial patch and Alex Gaynor, Carl Meyer, Donald Stufft, Jacob Kaplan-Moss and Julien Phalip for code reviewing.

* Added ability to pass the project or app directory path as the second argument
* Added ``--template`` option for specifying custom project and app templates
* Cleaned up admin_scripts tests a little while I was there

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17246 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent 98c974c commit a9a0f0b03f9a02deb03617bf7e9773a307d1328f @jezdez jezdez committed Dec 22, 2011
Showing with 996 additions and 266 deletions.
  1. +1 −1 django/conf/project_template/project_name/settings.py
  2. +1 −3 django/core/management/__init__.py
  3. +9 −73 django/core/management/base.py
  4. +3 −3 django/core/management/commands/makemessages.py
  5. +13 −16 django/core/management/commands/startapp.py
  6. +19 −24 django/core/management/commands/startproject.py
  7. +287 −0 django/core/management/templates.py
  8. +198 −0 django/utils/archive.py
  9. +9 −7 docs/man/django-admin.1
  10. +106 −6 docs/ref/django-admin.txt
  11. +21 −0 docs/releases/1.4-alpha-1.txt
  12. +21 −0 docs/releases/1.4.txt
  13. 0 tests/regressiontests/admin_scripts/custom_templates/app_template/__init__.py
  14. +1 −0 tests/regressiontests/admin_scripts/custom_templates/app_template/api.py
  15. +1 −0 tests/regressiontests/admin_scripts/custom_templates/app_template/models.py
  16. BIN tests/regressiontests/admin_scripts/custom_templates/project_template.tgz
  17. +1 −0 ...regressiontests/admin_scripts/custom_templates/project_template/additional_dir/additional_file.py
  18. +1 −0 tests/regressiontests/admin_scripts/custom_templates/project_template/manage.py
  19. 0 tests/regressiontests/admin_scripts/custom_templates/project_template/project_name/__init__.py
  20. +1 −0 tests/regressiontests/admin_scripts/custom_templates/project_template/project_name/settings.py
  21. +220 −133 tests/regressiontests/admin_scripts/tests.py
  22. +10 −0 tests/regressiontests/admin_scripts/urls.py
  23. +69 −0 tests/regressiontests/utils/archive.py
  24. BIN tests/regressiontests/utils/archives/foobar.tar
  25. BIN tests/regressiontests/utils/archives/foobar.tar.bz2
  26. BIN tests/regressiontests/utils/archives/foobar.tar.gz
  27. BIN tests/regressiontests/utils/archives/foobar.zip
  28. +1 −0 tests/regressiontests/utils/tests.py
  29. +3 −0 tests/urls.py
@@ -81,7 +81,7 @@
)
# Make this unique, and don't share it with anybody.
-SECRET_KEY = ''
+SECRET_KEY = '{{ secret_key }}'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
@@ -77,9 +77,7 @@ def get_commands():
in that package are registered.
Core commands are always included. If a settings module has been
- specified, user-defined commands will also be included, the
- startproject command will be disabled, and the startapp command
- will be modified to use the directory in which the settings module appears.
+ specified, user-defined commands will also be included.
The dictionary is in the format {command_name: app_name}. Key-value
pairs from this dictionary can then be used in calls to
@@ -3,9 +3,10 @@
be executed through ``django-admin.py`` or ``manage.py``).
"""
-
+from __future__ import with_statement
import os
import sys
+
from optparse import make_option, OptionParser
import traceback
@@ -14,6 +15,7 @@
from django.core.management.color import color_style
from django.utils.encoding import smart_str
+
class CommandError(Exception):
"""
Exception class indicating a problem while executing a management
@@ -29,6 +31,7 @@ class CommandError(Exception):
"""
pass
+
def handle_default_options(options):
"""
Include any default options that all commands should accept here
@@ -41,6 +44,7 @@ def handle_default_options(options):
if options.pythonpath:
sys.path.insert(0, options.pythonpath)
+
class BaseCommand(object):
"""
The base class from which all management commands ultimately
@@ -134,7 +138,7 @@ class BaseCommand(object):
# Configuration shortcuts that alter various logic.
can_import_settings = True
requires_model_validation = True
- output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
+ output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
def __init__(self):
self.style = color_style()
@@ -275,6 +279,7 @@ def handle(self, *args, **options):
"""
raise NotImplementedError()
+
class AppCommand(BaseCommand):
"""
A management command which takes one or more installed application
@@ -310,6 +315,7 @@ def handle_app(self, app, **options):
"""
raise NotImplementedError()
+
class LabelCommand(BaseCommand):
"""
A management command which takes one or more arbitrary arguments
@@ -345,6 +351,7 @@ def handle_label(self, label, **options):
"""
raise NotImplementedError()
+
class NoArgsCommand(BaseCommand):
"""
A command which takes no arguments on the command line.
@@ -369,74 +376,3 @@ def handle_noargs(self, **options):
"""
raise NotImplementedError()
-
-def copy_helper(style, app_or_project, name, directory):
- """
- Copies either a Django application layout template or a Django project
- layout template into the specified directory.
-
- """
- # style -- A color style object (see django.core.management.color).
- # app_or_project -- The string 'app' or 'project'.
- # name -- The name of the application or project.
- # directory -- The directory to which the layout template should be copied.
- import re
- import shutil
-
- if not re.search(r'^[_a-zA-Z]\w*$', name): # If it's not a valid directory name.
- # Provide a smart error message, depending on the error.
- if not re.search(r'^[_a-zA-Z]', name):
- message = 'make sure the name begins with a letter or underscore'
- else:
- message = 'use only numbers, letters and underscores'
- raise CommandError("%r is not a valid %s name. Please %s." % (name, app_or_project, message))
- top_dir = os.path.join(directory, name)
- try:
- os.mkdir(top_dir)
- except OSError, e:
- raise CommandError(e)
-
- # Determine where the app or project templates are. Use
- # django.__path__[0] because we don't know into which directory
- # django has been installed.
- template_dir = os.path.join(django.__path__[0], 'conf', '%s_template' % app_or_project)
-
- for d, subdirs, files in os.walk(template_dir):
- relative_dir = d[len(template_dir)+1:].replace('%s_name' % app_or_project, name)
- if relative_dir:
- os.mkdir(os.path.join(top_dir, relative_dir))
- for subdir in subdirs[:]:
- if subdir.startswith('.'):
- subdirs.remove(subdir)
- for f in files:
- if not f.endswith('.py'):
- # Ignore .pyc, .pyo, .py.class etc, as they cause various
- # breakages.
- continue
- path_old = os.path.join(d, f)
- path_new = os.path.join(top_dir, relative_dir, f.replace('%s_name' % app_or_project, name))
- fp_old = open(path_old, 'r')
- fp_new = open(path_new, 'w')
- fp_new.write(fp_old.read().replace('{{ %s_name }}' % app_or_project, name))
- fp_old.close()
- fp_new.close()
- try:
- shutil.copymode(path_old, path_new)
- _make_writeable(path_new)
- except OSError:
- sys.stderr.write(style.NOTICE("Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" % path_new))
-
-def _make_writeable(filename):
- """
- Make sure that the file is writeable. Useful if our source is
- read-only.
-
- """
- import stat
- if sys.platform.startswith('java'):
- # On Jython there is no os.access()
- return
- if not os.access(filename, os.W_OK):
- st = os.stat(filename)
- new_permissions = stat.S_IMODE(st.st_mode) | stat.S_IWUSR
- os.chmod(filename, new_permissions)
@@ -13,7 +13,7 @@
plural_forms_re = re.compile(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL)
-def handle_extensions(extensions=('html',)):
+def handle_extensions(extensions=('html',), ignored=('py',)):
"""
Organizes multiple extensions that are separated with commas or passed by
using --extension/-e multiple times. Note that the .py extension is ignored
@@ -31,11 +31,11 @@ def handle_extensions(extensions=('html',)):
"""
ext_list = []
for ext in extensions:
- ext_list.extend(ext.replace(' ','').split(','))
+ ext_list.extend(ext.replace(' ', '').split(','))
for i, ext in enumerate(ext_list):
if not ext.startswith('.'):
ext_list[i] = '.%s' % ext_list[i]
- return set([x for x in ext_list if x != '.py'])
+ return set([x for x in ext_list if x.strip('.') not in ignored])
def _popen(cmd):
"""
@@ -1,28 +1,25 @@
-import os
-
-from django.core.management.base import copy_helper, CommandError, LabelCommand
+from django.core.management.base import CommandError
+from django.core.management.templates import TemplateCommand
from django.utils.importlib import import_module
-class Command(LabelCommand):
- help = "Creates a Django app directory structure for the given app name in the current directory."
- args = "[appname]"
- label = 'application name'
- requires_model_validation = False
- # Can't import settings during this command, because they haven't
- # necessarily been created.
- can_import_settings = False
+class Command(TemplateCommand):
+ help = ("Creates a Django app directory structure for the given app "
+ "name in the current directory or optionally in the given "
+ "directory.")
- def handle_label(self, app_name, directory=None, **options):
- if directory is None:
- directory = os.getcwd()
+ def handle(self, app_name=None, target=None, **options):
+ if app_name is None:
+ raise CommandError("you must provide an app name")
# Check that the app_name cannot be imported.
try:
import_module(app_name)
except ImportError:
pass
else:
- raise CommandError("%r conflicts with the name of an existing Python module and cannot be used as an app name. Please try another name." % app_name)
+ raise CommandError("%r conflicts with the name of an existing "
+ "Python module and cannot be used as an app "
+ "name. Please try another name." % app_name)
- copy_helper(self.style, 'app', app_name, directory)
+ super(Command, self).handle('app', app_name, target, **options)
@@ -1,37 +1,32 @@
-from django.core.management.base import copy_helper, CommandError, LabelCommand
-from django.utils.importlib import import_module
-import os
-import re
from random import choice
-class Command(LabelCommand):
- help = "Creates a Django project directory structure for the given project name in the current directory."
- args = "[projectname]"
- label = 'project name'
+from django.core.management.base import CommandError
+from django.core.management.templates import TemplateCommand
+from django.utils.importlib import import_module
+
- requires_model_validation = False
- # Can't import settings during this command, because they haven't
- # necessarily been created.
- can_import_settings = False
+class Command(TemplateCommand):
+ help = ("Creates a Django project directory structure for the given "
+ "project name in the current directory or optionally in the "
+ "given directory.")
- def handle_label(self, project_name, **options):
- directory = os.getcwd()
+ def handle(self, project_name=None, target=None, *args, **options):
+ if project_name is None:
+ raise CommandError("you must provide a project name")
# Check that the project_name cannot be imported.
try:
import_module(project_name)
except ImportError:
pass
else:
- raise CommandError("%r conflicts with the name of an existing Python module and cannot be used as a project name. Please try another name." % project_name)
+ raise CommandError("%r conflicts with the name of an existing "
+ "Python module and cannot be used as a "
+ "project name. Please try another name." %
+ project_name)
- copy_helper(self.style, 'project', project_name, directory)
+ # Create a random SECRET_KEY hash to put it in the main settings.
+ chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
+ options['secret_key'] = ''.join([choice(chars) for i in range(50)])
- # Create a random SECRET_KEY hash, and put it in the main settings.
- main_settings_file = os.path.join(directory, project_name, project_name, 'settings.py')
- settings_contents = open(main_settings_file, 'r').read()
- fp = open(main_settings_file, 'w')
- secret_key = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)])
- settings_contents = re.sub(r"(?<=SECRET_KEY = ')'", secret_key + "'", settings_contents)
- fp.write(settings_contents)
- fp.close()
+ super(Command, self).handle('project', project_name, target, **options)
Oops, something went wrong.

0 comments on commit a9a0f0b

Please sign in to comment.