diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/config/settings/base.py index 274e2806..efe1127b 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/config/settings/base.py @@ -53,6 +53,7 @@ THIRD_PARTY_APPS = [ # Apps specific for this project go here. LOCAL_APPS = [ + '{{ cookiecutter.project_slug }}.utils.apps.UtilsConfig', # custom users app '{{ cookiecutter.project_slug }}.users.apps.UsersConfig', # Your stuff: custom apps go here diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/apps.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/apps.py new file mode 100644 index 00000000..204d9f5f --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig + + +class UtilsConfig(AppConfig): + name = '{{cookiecutter.project_slug}}.utils' + verbose_name = "Utils" + + def ready(self): + """Override this to put in: + Utils system checks + Utils signal registration + """ + pass diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/commands/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/commands/startapp2.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/commands/startapp2.py new file mode 100644 index 00000000..35547542 --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/management/commands/startapp2.py @@ -0,0 +1,170 @@ +import os +import shutil +from importlib import import_module +from os import path + +import django +from django.core.management.base import CommandError +from django.core.management.templates import TemplateCommand +from django.core.management.utils import handle_extensions +from django.template import Context, Engine +from django.utils.version import get_docs_version + +# Setup a stub settings environment for template rendering +from django.conf import settings + +if not settings.configured: + settings.configure() + +COOKIECUTTER_DJANGO_APPS_PATH = str(settings.APPS_DIR) +COOKIECUTTER_DJANGO_APPS_DIR_NAME = os.path.basename(COOKIECUTTER_DJANGO_APPS_PATH) +COOKIECUTTER_DJANGO_BASE_SETTINGS_FILE = str(settings.ROOT_DIR.path('config', 'settings', 'base.py')) + + +class Command(TemplateCommand): + help = ( + "Creates a Django app directory structure for the given app name in " + "the {dir}/ directory.".format(dir=COOKIECUTTER_DJANGO_APPS_PATH) + ) + missing_args_message = "You must provide an application name." + + 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') + parser.add_argument('--template', + help='The path or URL to load the template from.') + parser.add_argument('--extension', '-e', dest='extensions', + action='append', default=['py'], + help='The file extension(s) to render (default: "py"). ' + 'Separate multiple extensions with commas, or use ' + '-e multiple times.') + parser.add_argument('--name', '-n', dest='files', + action='append', default=[], + help='The file name(s) to render. ' + 'Separate multiple extensions with commas, or use ' + '-n multiple times.') + + def handle(self, **options): + name = options.pop('name') + self.app_or_project = 'app' + self.paths_to_remove = [] + self.verbosity = options['verbosity'] + + self.validate_name(name, self.app_or_project) + dot_path_base_name = '{}.{}'.format(COOKIECUTTER_DJANGO_APPS_DIR_NAME, name) + # Check that the app_name cannot be imported. + try: + import_module(dot_path_base_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." % name + ) + + top_dir = path.join(COOKIECUTTER_DJANGO_APPS_PATH, name) + try: + os.makedirs(top_dir) + except FileExistsError: + raise CommandError("'%s' already exists" % top_dir) + except OSError as e: + raise CommandError(e) + + extensions = tuple(handle_extensions(options['extensions'])) + extra_files = [] + for file in options['files']: + extra_files.extend(map(lambda x: x.strip(), file.split(','))) + if self.verbosity >= 2: + self.stdout.write("Rendering %s template files with " + "extensions: %s\n" % + (self.app_or_project, ', '.join(extensions))) + self.stdout.write("Rendering %s template files with " + "filenames: %s\n" % + (self.app_or_project, ', '.join(extra_files))) + + base_name = '%s_name' % self.app_or_project + base_subdir = '%s_template' % self.app_or_project + base_directory = '%s_directory' % self.app_or_project + camel_case_name = 'camel_case_%s_name' % self.app_or_project + camel_case_value = ''.join(x for x in name.title() if x != '_') + + context = Context(dict(options, **{ + base_name: dot_path_base_name, + base_directory: top_dir, + camel_case_name: camel_case_value, + 'docs_version': get_docs_version(), + 'django_version': django.__version__, + }), autoescape=False) + + template_dir = self.handle_template(options['template'], + base_subdir) + prefix_length = len(template_dir) + 1 + + for root, dirs, files in os.walk(template_dir): + + path_rest = root[prefix_length:] + relative_dir = path_rest.replace(base_name, name) + if relative_dir: + target_dir = path.join(top_dir, relative_dir) + if not path.exists(target_dir): + os.mkdir(target_dir) + + for dirname in dirs[:]: + if dirname.startswith('.') or dirname == '__pycache__': + dirs.remove(dirname) + + for filename in files: + if filename.endswith(('.pyo', '.pyc', '.py.class')): + # Ignore some files as they cause various breakages. + continue + old_path = path.join(root, filename) + new_path = path.join(top_dir, relative_dir, + filename.replace(base_name, name)) + for old_suffix, new_suffix in self.rewrite_template_suffixes: + if new_path.endswith(old_suffix): + new_path = new_path[:-len(old_suffix)] + new_suffix + break # Only rewrite once + + if path.exists(new_path): + raise CommandError("%s already exists, overlaying a " + "project or app into an existing " + "directory won't replace conflicting " + "files" % new_path) + + # Only render the Python files, as we don't want to + # accidentally render Django templates files + if new_path.endswith(extensions) or filename in extra_files: + with open(old_path, 'r', encoding='utf-8') as template_file: + content = template_file.read() + template = Engine().from_string(content) + content = template.render(context) + with open(new_path, 'w', encoding='utf-8') as new_file: + new_file.write(content) + else: + shutil.copyfile(old_path, new_path) + + if self.verbosity >= 2: + self.stdout.write("Creating %s\n" % new_path) + try: + shutil.copymode(old_path, new_path) + self.make_writeable(new_path) + except OSError: + self.stderr.write( + "Notice: Couldn't set permission bits on %s. You're " + "probably using an uncommon filesystem setup. No " + "problem." % new_path, self.style.NOTICE) + + if self.paths_to_remove: + if self.verbosity >= 2: + self.stdout.write("Cleaning up temporary files.\n") + for path_to_remove in self.paths_to_remove: + if path.isfile(path_to_remove): + os.remove(path_to_remove) + else: + shutil.rmtree(path_to_remove) + + msg = "'{dot_path_base_name}.apps.{camel_case_value}Config',".format(dot_path_base_name=dot_path_base_name, + camel_case_value=camel_case_value) + self.stdout.write( + 'please, add:\n\n{}\n\nto your LOCAL_APPS on {}\n'.format(msg, COOKIECUTTER_DJANGO_BASE_SETTINGS_FILE))