Commit a9a0f0b0 authored by Jannis Leidel's avatar Jannis Leidel
Browse files

Fixed #17042 -- Extended startproject and startapp management commands to...

Fixed #17042 -- Extended startproject and startapp management commands 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
parent 98c974c7
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -81,7 +81,7 @@ STATICFILES_FINDERS = (
)

# 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 = (
+1 −3
Original line number Diff line number Diff line
@@ -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
+9 −73
Original line number Diff line number Diff line
@@ -3,9 +3,10 @@ Base classes for writing management commands (named commands which can
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.exceptions import ImproperlyConfigured
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
@@ -275,6 +279,7 @@ class BaseCommand(object):
        """
        raise NotImplementedError()


class AppCommand(BaseCommand):
    """
    A management command which takes one or more installed application
@@ -310,6 +315,7 @@ class AppCommand(BaseCommand):
        """
        raise NotImplementedError()


class LabelCommand(BaseCommand):
    """
    A management command which takes one or more arbitrary arguments
@@ -345,6 +351,7 @@ class LabelCommand(BaseCommand):
        """
        raise NotImplementedError()


class NoArgsCommand(BaseCommand):
    """
    A command which takes no arguments on the command line.
@@ -369,74 +376,3 @@ class NoArgsCommand(BaseCommand):

        """
        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)
+3 −3
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ from django.utils.jslex import prepare_js_for_gettext

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
@@ -35,7 +35,7 @@ def handle_extensions(extensions=('html',)):
    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):
    """
+13 −16
Original line number Diff line number Diff line
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:
@@ -23,6 +18,8 @@ class Command(LabelCommand):
        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)
Loading