1 from __future__
import with_statement
14 from optparse
import make_option
18 from django
.template
import Template
, Context
19 from django
.utils
import archive
20 from django
.utils
.encoding
import smart_str
21 from django
.utils
._os
import rmtree_errorhandler
22 from django
.core
.management
.base
import BaseCommand
, CommandError
23 from django
.core
.management
.commands
.makemessages
import handle_extensions
26 _drive_re
= re
.compile('^([a-z]):', re
.I
)
27 _url_drive_re
= re
.compile('^([a-z])[:|]', re
.I
)
30 class TemplateCommand(BaseCommand
):
32 Copies either a Django application layout template or a Django project
33 layout template into the specified directory.
35 :param style: A color style object (see django.core.management.color).
36 :param app_or_project: The string 'app' or 'project'.
37 :param name: The name of the application or project.
38 :param directory: The directory to which the template should be copied.
39 :param options: The additional variables passed to project or app templates
41 args
= "[name] [optional destination directory]"
42 option_list
= BaseCommand
.option_list
+ (
43 make_option('--template',
44 action
='store', dest
='template',
45 help='The dotted import path to load the template from.'),
46 make_option('--extension', '-e', dest
='extensions',
47 action
='append', default
=['py'],
48 help='The file extension(s) to render (default: "py"). '
49 'Separate multiple extensions with commas, or use '
50 '-e multiple times.'),
51 make_option('--name', '-n', dest
='files',
52 action
='append', default
=[],
53 help='The file name(s) to render. '
54 'Separate multiple extensions with commas, or use '
57 requires_model_validation
= False
58 # Can't import settings during this command, because they haven't
59 # necessarily been created.
60 can_import_settings
= False
61 # The supported URL schemes
62 url_schemes
= ['http', 'https', 'ftp']
64 def handle(self
, app_or_project
, name
, target
=None, **options
):
65 self
.app_or_project
= app_or_project
66 self
.paths_to_remove
= []
67 self
.verbosity
= int(options
.get('verbosity'))
69 # If it's not a valid directory name.
70 if not re
.search(r
'^[_a-zA-Z]\w*$', name
):
71 # Provide a smart error message, depending on the error.
72 if not re
.search(r
'^[_a-zA-Z]', name
):
73 message
= ('make sure the name begins '
74 'with a letter or underscore')
76 message
= 'use only numbers, letters and underscores'
77 raise CommandError("%r is not a valid %s name. Please %s." %
78 (name
, app_or_project
, message
))
80 # if some directory is given, make sure it's nicely expanded
82 top_dir
= path
.join(os
.getcwd(), name
)
86 if e
.errno
== errno
.EEXIST
:
87 message
= "'%s' already exists" % top_dir
90 raise CommandError(message
)
92 top_dir
= os
.path
.abspath(path
.expanduser(target
))
93 if not os
.path
.exists(top_dir
):
94 raise CommandError("Destination directory '%s' does not "
95 "exist, please create it first." % top_dir
)
98 handle_extensions(options
.get('extensions'), ignored
=()))
100 for file in options
.get('files'):
101 extra_files
.extend(map(lambda x
: x
.strip(), file.split(',')))
102 if self
.verbosity
>= 2:
103 self
.stdout
.write("Rendering %s template files with "
105 (app_or_project
, ', '.join(extensions
)))
106 self
.stdout
.write("Rendering %s template files with "
108 (app_or_project
, ', '.join(extra_files
)))
110 base_name
= '%s_name' % app_or_project
111 base_subdir
= '%s_template' % app_or_project
112 base_directory
= '%s_directory' % app_or_project
114 context
= Context(dict(options
, **{
116 base_directory
: top_dir
,
119 # Setup a stub settings environment for template rendering
120 from django
.conf
import settings
121 if not settings
.configured
:
124 template_dir
= self
.handle_template(options
.get('template'),
126 prefix_length
= len(template_dir
) + 1
128 for root
, dirs
, files
in os
.walk(template_dir
):
130 path_rest
= root
[prefix_length
:]
131 relative_dir
= path_rest
.replace(base_name
, name
)
133 target_dir
= path
.join(top_dir
, relative_dir
)
134 if not path
.exists(target_dir
):
137 for dirname
in dirs
[:]:
138 if dirname
.startswith('.'):
141 for filename
in files
:
142 if filename
.endswith(('.pyo', '.pyc', '.py.class')):
143 # Ignore some files as they cause various breakages.
145 old_path
= path
.join(root
, filename
)
146 new_path
= path
.join(top_dir
, relative_dir
,
147 filename
.replace(base_name
, name
))
148 if path
.exists(new_path
):
149 raise CommandError("%s already exists, overlaying a "
150 "project or app into an existing "
151 "directory won't replace conflicting "
154 # Only render the Python files, as we don't want to
155 # accidentally render Django templates files
156 with
open(old_path
, 'r') as template_file
:
157 content
= template_file
.read()
158 if filename
.endswith(extensions
) or filename
in extra_files
:
159 template
= Template(content
)
160 content
= template
.render(context
)
161 with
open(new_path
, 'w') as new_file
:
162 new_file
.write(content
)
164 if self
.verbosity
>= 2:
165 self
.stdout
.write("Creating %s\n" % new_path
)
167 shutil
.copymode(old_path
, new_path
)
168 self
.make_writeable(new_path
)
170 notice
= self
.style
.NOTICE(
171 "Notice: Couldn't set permission bits on %s. You're "
172 "probably using an uncommon filesystem setup. No "
173 "problem.\n" % new_path
)
174 sys
.stderr
.write(smart_str(notice
))
176 if self
.paths_to_remove
:
177 if self
.verbosity
>= 2:
178 self
.stdout
.write("Cleaning up temporary files.\n")
179 for path_to_remove
in self
.paths_to_remove
:
180 if path
.isfile(path_to_remove
):
181 os
.remove(path_to_remove
)
183 shutil
.rmtree(path_to_remove
,
184 onerror
=rmtree_errorhandler
)
186 def handle_template(self
, template
, subdir
):
188 Determines where the app or project templates are.
189 Use django.__path__[0] as the default because we don't
190 know into which directory Django has been installed.
193 return path
.join(django
.__path
__[0], 'conf', subdir
)
195 if template
.startswith('file://'):
196 template
= template
[7:]
197 expanded_template
= path
.expanduser(template
)
198 expanded_template
= path
.normpath(expanded_template
)
199 if path
.isdir(expanded_template
):
200 return expanded_template
201 if self
.is_url(template
):
202 # downloads the file and returns the path
203 absolute_path
= self
.download(template
)
205 absolute_path
= path
.abspath(expanded_template
)
206 if path
.exists(absolute_path
):
207 return self
.extract(absolute_path
)
209 raise CommandError("couldn't handle %s template %s." %
210 (self
.app_or_project
, template
))
212 def download(self
, url
):
214 Downloads the given URL and returns the file name.
216 def cleanup_url(url
):
217 tmp
= url
.rstrip('/')
218 filename
= tmp
.split('/')[-1]
219 if url
.endswith('/'):
220 display_url
= tmp
+ '/'
223 return filename
, display_url
225 prefix
= 'django_%s_template_' % self
.app_or_project
226 tempdir
= tempfile
.mkdtemp(prefix
=prefix
, suffix
='_download')
227 self
.paths_to_remove
.append(tempdir
)
228 filename
, display_url
= cleanup_url(url
)
230 if self
.verbosity
>= 2:
231 self
.stdout
.write("Downloading %s\n" % display_url
)
233 the_path
, info
= urllib
.urlretrieve(url
,
234 path
.join(tempdir
, filename
))
236 raise CommandError("couldn't download URL %s to %s: %s" %
239 used_name
= the_path
.split('/')[-1]
241 # Trying to get better name from response headers
242 content_disposition
= info
.get('content-disposition')
243 if content_disposition
:
244 _
, params
= cgi
.parse_header(content_disposition
)
245 guessed_filename
= params
.get('filename') or used_name
247 guessed_filename
= used_name
249 # Falling back to content type guessing
250 ext
= self
.splitext(guessed_filename
)[1]
251 content_type
= info
.get('content-type')
252 if not ext
and content_type
:
253 ext
= mimetypes
.guess_extension(content_type
)
255 guessed_filename
+= ext
257 # Move the temporary file to a filename that has better
258 # chances of being recognnized by the archive utils
259 if used_name
!= guessed_filename
:
260 guessed_path
= path
.join(tempdir
, guessed_filename
)
261 shutil
.move(the_path
, guessed_path
)
267 def splitext(self
, the_path
):
269 Like os.path.splitext, but takes off .tar, too
271 base
, ext
= posixpath
.splitext(the_path
)
272 if base
.lower().endswith('.tar'):
273 ext
= base
[-4:] + ext
277 def extract(self
, filename
):
279 Extracts the given file to a temporarily and returns
280 the path of the directory with the extracted content.
282 prefix
= 'django_%s_template_' % self
.app_or_project
283 tempdir
= tempfile
.mkdtemp(prefix
=prefix
, suffix
='_extract')
284 self
.paths_to_remove
.append(tempdir
)
285 if self
.verbosity
>= 2:
286 self
.stdout
.write("Extracting %s\n" % filename
)
288 archive
.extract(filename
, tempdir
)
290 except (archive
.ArchiveException
, IOError), e
:
291 raise CommandError("couldn't extract file %s to %s: %s" %
292 (filename
, tempdir
, e
))
294 def is_url(self
, template
):
296 Returns True if the name looks like a URL
298 if ':' not in template
:
300 scheme
= template
.split(':', 1)[0].lower()
301 return scheme
in self
.url_schemes
303 def make_writeable(self
, filename
):
305 Make sure that the file is writeable.
306 Useful if our source is read-only.
308 if sys
.platform
.startswith('java'):
309 # On Jython there is no os.access()
311 if not os
.access(filename
, os
.W_OK
):
312 st
= os
.stat(filename
)
313 new_permissions
= stat
.S_IMODE(st
.st_mode
) | stat
.S_IWUSR
314 os
.chmod(filename
, new_permissions
)