| 1 |
from django import template, templatetags |
|---|
| 2 |
from django.template import RequestContext |
|---|
| 3 |
from django.conf import settings |
|---|
| 4 |
from django.contrib.admin.views.decorators import staff_member_required |
|---|
| 5 |
from django.db import models |
|---|
| 6 |
from django.shortcuts import render_to_response |
|---|
| 7 |
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist |
|---|
| 8 |
from django.http import Http404, get_host |
|---|
| 9 |
from django.core import urlresolvers |
|---|
| 10 |
from django.contrib.admindocs import utils |
|---|
| 11 |
from django.contrib.sites.models import Site |
|---|
| 12 |
from django.utils.translation import ugettext as _ |
|---|
| 13 |
from django.utils.safestring import mark_safe |
|---|
| 14 |
import inspect, os, re |
|---|
| 15 |
|
|---|
| 16 |
# Exclude methods starting with these strings from documentation |
|---|
| 17 |
MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_') |
|---|
| 18 |
|
|---|
| 19 |
class GenericSite(object): |
|---|
| 20 |
domain = 'example.com' |
|---|
| 21 |
name = 'my site' |
|---|
| 22 |
|
|---|
| 23 |
def doc_index(request): |
|---|
| 24 |
if not utils.docutils_is_available: |
|---|
| 25 |
return missing_docutils_page(request) |
|---|
| 26 |
root_path = re.sub(re.escape('doc/') + '$', '', request.path) |
|---|
| 27 |
return render_to_response('admin_doc/index.html', { |
|---|
| 28 |
'root_path': root_path, |
|---|
| 29 |
}, context_instance=RequestContext(request)) |
|---|
| 30 |
doc_index = staff_member_required(doc_index) |
|---|
| 31 |
|
|---|
| 32 |
def bookmarklets(request): |
|---|
| 33 |
# Hack! This couples this view to the URL it lives at. |
|---|
| 34 |
admin_root = request.path[:-len('doc/bookmarklets/')] |
|---|
| 35 |
root_path = re.sub(re.escape('doc/bookmarklets/') + '$', '', request.path) |
|---|
| 36 |
return render_to_response('admin_doc/bookmarklets.html', { |
|---|
| 37 |
'root_path': root_path, |
|---|
| 38 |
'admin_url': mark_safe("%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root)), |
|---|
| 39 |
}, context_instance=RequestContext(request)) |
|---|
| 40 |
bookmarklets = staff_member_required(bookmarklets) |
|---|
| 41 |
|
|---|
| 42 |
def template_tag_index(request): |
|---|
| 43 |
if not utils.docutils_is_available: |
|---|
| 44 |
return missing_docutils_page(request) |
|---|
| 45 |
|
|---|
| 46 |
load_all_installed_template_libraries() |
|---|
| 47 |
|
|---|
| 48 |
tags = [] |
|---|
| 49 |
for module_name, library in template.libraries.items(): |
|---|
| 50 |
for tag_name, tag_func in library.tags.items(): |
|---|
| 51 |
title, body, metadata = utils.parse_docstring(tag_func.__doc__) |
|---|
| 52 |
if title: |
|---|
| 53 |
title = utils.parse_rst(title, 'tag', _('tag:') + tag_name) |
|---|
| 54 |
if body: |
|---|
| 55 |
body = utils.parse_rst(body, 'tag', _('tag:') + tag_name) |
|---|
| 56 |
for key in metadata: |
|---|
| 57 |
metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name) |
|---|
| 58 |
if library in template.builtins: |
|---|
| 59 |
tag_library = None |
|---|
| 60 |
else: |
|---|
| 61 |
tag_library = module_name.split('.')[-1] |
|---|
| 62 |
tags.append({ |
|---|
| 63 |
'name': tag_name, |
|---|
| 64 |
'title': title, |
|---|
| 65 |
'body': body, |
|---|
| 66 |
'meta': metadata, |
|---|
| 67 |
'library': tag_library, |
|---|
| 68 |
}) |
|---|
| 69 |
root_path = re.sub(re.escape('doc/tags/') + '$', '', request.path) |
|---|
| 70 |
return render_to_response('admin_doc/template_tag_index.html', { |
|---|
| 71 |
'root_path': root_path, |
|---|
| 72 |
'tags': tags |
|---|
| 73 |
}, context_instance=RequestContext(request)) |
|---|
| 74 |
template_tag_index = staff_member_required(template_tag_index) |
|---|
| 75 |
|
|---|
| 76 |
def template_filter_index(request): |
|---|
| 77 |
if not utils.docutils_is_available: |
|---|
| 78 |
return missing_docutils_page(request) |
|---|
| 79 |
|
|---|
| 80 |
load_all_installed_template_libraries() |
|---|
| 81 |
|
|---|
| 82 |
filters = [] |
|---|
| 83 |
for module_name, library in template.libraries.items(): |
|---|
| 84 |
for filter_name, filter_func in library.filters.items(): |
|---|
| 85 |
title, body, metadata = utils.parse_docstring(filter_func.__doc__) |
|---|
| 86 |
if title: |
|---|
| 87 |
title = utils.parse_rst(title, 'filter', _('filter:') + filter_name) |
|---|
| 88 |
if body: |
|---|
| 89 |
body = utils.parse_rst(body, 'filter', _('filter:') + filter_name) |
|---|
| 90 |
for key in metadata: |
|---|
| 91 |
metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name) |
|---|
| 92 |
if library in template.builtins: |
|---|
| 93 |
tag_library = None |
|---|
| 94 |
else: |
|---|
| 95 |
tag_library = module_name.split('.')[-1] |
|---|
| 96 |
filters.append({ |
|---|
| 97 |
'name': filter_name, |
|---|
| 98 |
'title': title, |
|---|
| 99 |
'body': body, |
|---|
| 100 |
'meta': metadata, |
|---|
| 101 |
'library': tag_library, |
|---|
| 102 |
}) |
|---|
| 103 |
root_path = re.sub(re.escape('doc/filters/') + '$', '', request.path) |
|---|
| 104 |
return render_to_response('admin_doc/template_filter_index.html', { |
|---|
| 105 |
'root_path': root_path, |
|---|
| 106 |
'filters': filters |
|---|
| 107 |
}, context_instance=RequestContext(request)) |
|---|
| 108 |
template_filter_index = staff_member_required(template_filter_index) |
|---|
| 109 |
|
|---|
| 110 |
def view_index(request): |
|---|
| 111 |
if not utils.docutils_is_available: |
|---|
| 112 |
return missing_docutils_page(request) |
|---|
| 113 |
|
|---|
| 114 |
if settings.ADMIN_FOR: |
|---|
| 115 |
settings_modules = [__import__(m, {}, {}, ['']) for m in settings.ADMIN_FOR] |
|---|
| 116 |
else: |
|---|
| 117 |
settings_modules = [settings] |
|---|
| 118 |
|
|---|
| 119 |
views = [] |
|---|
| 120 |
for settings_mod in settings_modules: |
|---|
| 121 |
urlconf = __import__(settings_mod.ROOT_URLCONF, {}, {}, ['']) |
|---|
| 122 |
view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns) |
|---|
| 123 |
if Site._meta.installed: |
|---|
| 124 |
site_obj = Site.objects.get(pk=settings_mod.SITE_ID) |
|---|
| 125 |
else: |
|---|
| 126 |
site_obj = GenericSite() |
|---|
| 127 |
for (func, regex) in view_functions: |
|---|
| 128 |
views.append({ |
|---|
| 129 |
'name': func.__name__, |
|---|
| 130 |
'module': func.__module__, |
|---|
| 131 |
'site_id': settings_mod.SITE_ID, |
|---|
| 132 |
'site': site_obj, |
|---|
| 133 |
'url': simplify_regex(regex), |
|---|
| 134 |
}) |
|---|
| 135 |
root_path = re.sub(re.escape('doc/views/') + '$', '', request.path) |
|---|
| 136 |
return render_to_response('admin_doc/view_index.html', { |
|---|
| 137 |
'root_path': root_path, |
|---|
| 138 |
'views': views |
|---|
| 139 |
}, context_instance=RequestContext(request)) |
|---|
| 140 |
view_index = staff_member_required(view_index) |
|---|
| 141 |
|
|---|
| 142 |
def view_detail(request, view): |
|---|
| 143 |
if not utils.docutils_is_available: |
|---|
| 144 |
return missing_docutils_page(request) |
|---|
| 145 |
|
|---|
| 146 |
mod, func = urlresolvers.get_mod_func(view) |
|---|
| 147 |
try: |
|---|
| 148 |
view_func = getattr(__import__(mod, {}, {}, ['']), func) |
|---|
| 149 |
except (ImportError, AttributeError): |
|---|
| 150 |
raise Http404 |
|---|
| 151 |
title, body, metadata = utils.parse_docstring(view_func.__doc__) |
|---|
| 152 |
if title: |
|---|
| 153 |
title = utils.parse_rst(title, 'view', _('view:') + view) |
|---|
| 154 |
if body: |
|---|
| 155 |
body = utils.parse_rst(body, 'view', _('view:') + view) |
|---|
| 156 |
for key in metadata: |
|---|
| 157 |
metadata[key] = utils.parse_rst(metadata[key], 'model', _('view:') + view) |
|---|
| 158 |
root_path = re.sub(re.escape('doc/views/%s/' % view) + '$', '', request.path) |
|---|
| 159 |
return render_to_response('admin_doc/view_detail.html', { |
|---|
| 160 |
'root_path': root_path, |
|---|
| 161 |
'name': view, |
|---|
| 162 |
'summary': title, |
|---|
| 163 |
'body': body, |
|---|
| 164 |
'meta': metadata, |
|---|
| 165 |
}, context_instance=RequestContext(request)) |
|---|
| 166 |
view_detail = staff_member_required(view_detail) |
|---|
| 167 |
|
|---|
| 168 |
def model_index(request): |
|---|
| 169 |
if not utils.docutils_is_available: |
|---|
| 170 |
return missing_docutils_page(request) |
|---|
| 171 |
m_list = [m._meta for m in models.get_models()] |
|---|
| 172 |
root_path = re.sub(re.escape('doc/models/') + '$', '', request.path) |
|---|
| 173 |
return render_to_response('admin_doc/model_index.html', { |
|---|
| 174 |
'root_path': root_path, |
|---|
| 175 |
'models': m_list |
|---|
| 176 |
}, context_instance=RequestContext(request)) |
|---|
| 177 |
model_index = staff_member_required(model_index) |
|---|
| 178 |
|
|---|
| 179 |
def model_detail(request, app_label, model_name): |
|---|
| 180 |
if not utils.docutils_is_available: |
|---|
| 181 |
return missing_docutils_page(request) |
|---|
| 182 |
|
|---|
| 183 |
# Get the model class. |
|---|
| 184 |
try: |
|---|
| 185 |
app_mod = models.get_app(app_label) |
|---|
| 186 |
except ImproperlyConfigured: |
|---|
| 187 |
raise Http404, _("App %r not found") % app_label |
|---|
| 188 |
model = None |
|---|
| 189 |
for m in models.get_models(app_mod): |
|---|
| 190 |
if m._meta.object_name.lower() == model_name: |
|---|
| 191 |
model = m |
|---|
| 192 |
break |
|---|
| 193 |
if model is None: |
|---|
| 194 |
raise Http404, _("Model %(model_name)r not found in app %(app_label)r") % {'model_name': model_name, 'app_label': app_label} |
|---|
| 195 |
|
|---|
| 196 |
opts = model._meta |
|---|
| 197 |
|
|---|
| 198 |
# Gather fields/field descriptions. |
|---|
| 199 |
fields = [] |
|---|
| 200 |
for field in opts.fields: |
|---|
| 201 |
# ForeignKey is a special case since the field will actually be a |
|---|
| 202 |
# descriptor that returns the other object |
|---|
| 203 |
if isinstance(field, models.ForeignKey): |
|---|
| 204 |
data_type = related_object_name = field.rel.to.__name__ |
|---|
| 205 |
app_label = field.rel.to._meta.app_label |
|---|
| 206 |
verbose = utils.parse_rst((_("the related `%(app_label)s.%(data_type)s` object") % {'app_label': app_label, 'data_type': data_type}), 'model', _('model:') + data_type) |
|---|
| 207 |
else: |
|---|
| 208 |
data_type = get_readable_field_data_type(field) |
|---|
| 209 |
verbose = field.verbose_name |
|---|
| 210 |
fields.append({ |
|---|
| 211 |
'name': field.name, |
|---|
| 212 |
'data_type': data_type, |
|---|
| 213 |
'verbose': verbose, |
|---|
| 214 |
'help_text': field.help_text, |
|---|
| 215 |
}) |
|---|
| 216 |
|
|---|
| 217 |
# Gather model methods. |
|---|
| 218 |
for func_name, func in model.__dict__.items(): |
|---|
| 219 |
if (inspect.isfunction(func) and len(inspect.getargspec(func)[0]) == 1): |
|---|
| 220 |
try: |
|---|
| 221 |
for exclude in MODEL_METHODS_EXCLUDE: |
|---|
| 222 |
if func_name.startswith(exclude): |
|---|
| 223 |
raise StopIteration |
|---|
| 224 |
except StopIteration: |
|---|
| 225 |
continue |
|---|
| 226 |
verbose = func.__doc__ |
|---|
| 227 |
if verbose: |
|---|
| 228 |
verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.module_name) |
|---|
| 229 |
fields.append({ |
|---|
| 230 |
'name': func_name, |
|---|
| 231 |
'data_type': get_return_data_type(func_name), |
|---|
| 232 |
'verbose': verbose, |
|---|
| 233 |
}) |
|---|
| 234 |
|
|---|
| 235 |
# Gather related objects |
|---|
| 236 |
for rel in opts.get_all_related_objects(): |
|---|
| 237 |
verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name} |
|---|
| 238 |
accessor = rel.get_accessor_name() |
|---|
| 239 |
fields.append({ |
|---|
| 240 |
'name' : "%s.all" % accessor, |
|---|
| 241 |
'data_type' : 'List', |
|---|
| 242 |
'verbose' : utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), |
|---|
| 243 |
}) |
|---|
| 244 |
fields.append({ |
|---|
| 245 |
'name' : "%s.count" % accessor, |
|---|
| 246 |
'data_type' : 'Integer', |
|---|
| 247 |
'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), |
|---|
| 248 |
}) |
|---|
| 249 |
root_path = re.sub(re.escape('doc/models/%s.%s/' % (app_label, model_name)) + '$', '', request.path) |
|---|
| 250 |
return render_to_response('admin_doc/model_detail.html', { |
|---|
| 251 |
'root_path': root_path, |
|---|
| 252 |
'name': '%s.%s' % (opts.app_label, opts.object_name), |
|---|
| 253 |
'summary': _("Fields on %s objects") % opts.object_name, |
|---|
| 254 |
'description': model.__doc__, |
|---|
| 255 |
'fields': fields, |
|---|
| 256 |
}, context_instance=RequestContext(request)) |
|---|
| 257 |
model_detail = staff_member_required(model_detail) |
|---|
| 258 |
|
|---|
| 259 |
def template_detail(request, template): |
|---|
| 260 |
templates = [] |
|---|
| 261 |
for site_settings_module in settings.ADMIN_FOR: |
|---|
| 262 |
settings_mod = __import__(site_settings_module, {}, {}, ['']) |
|---|
| 263 |
if Site._meta.installed: |
|---|
| 264 |
site_obj = Site.objects.get(pk=settings_mod.SITE_ID) |
|---|
| 265 |
else: |
|---|
| 266 |
site_obj = GenericSite() |
|---|
| 267 |
for dir in settings_mod.TEMPLATE_DIRS: |
|---|
| 268 |
template_file = os.path.join(dir, "%s.html" % template) |
|---|
| 269 |
templates.append({ |
|---|
| 270 |
'file': template_file, |
|---|
| 271 |
'exists': os.path.exists(template_file), |
|---|
| 272 |
'contents': lambda: os.path.exists(template_file) and open(template_file).read() or '', |
|---|
| 273 |
'site_id': settings_mod.SITE_ID, |
|---|
| 274 |
'site': site_obj, |
|---|
| 275 |
'order': list(settings_mod.TEMPLATE_DIRS).index(dir), |
|---|
| 276 |
}) |
|---|
| 277 |
root_path = re.sub(re.escape('doc/templates/%s/' % template) + '$', '', request.path) |
|---|
| 278 |
return render_to_response('admin_doc/template_detail.html', { |
|---|
| 279 |
'root_path': root_path, |
|---|
| 280 |
'name': template, |
|---|
| 281 |
'templates': templates, |
|---|
| 282 |
}, context_instance=RequestContext(request)) |
|---|
| 283 |
template_detail = staff_member_required(template_detail) |
|---|
| 284 |
|
|---|
| 285 |
#################### |
|---|
| 286 |
# Helper functions # |
|---|
| 287 |
#################### |
|---|
| 288 |
|
|---|
| 289 |
def missing_docutils_page(request): |
|---|
| 290 |
"""Display an error message for people without docutils""" |
|---|
| 291 |
return render_to_response('admin_doc/missing_docutils.html') |
|---|
| 292 |
|
|---|
| 293 |
def load_all_installed_template_libraries(): |
|---|
| 294 |
# Load/register all template tag libraries from installed apps. |
|---|
| 295 |
for e in templatetags.__path__: |
|---|
| 296 |
libraries = [os.path.splitext(p)[0] for p in os.listdir(e) if p.endswith('.py') and p[0].isalpha()] |
|---|
| 297 |
for library_name in libraries: |
|---|
| 298 |
try: |
|---|
| 299 |
lib = template.get_library("django.templatetags.%s" % library_name.split('.')[-1]) |
|---|
| 300 |
except template.InvalidTemplateLibrary: |
|---|
| 301 |
pass |
|---|
| 302 |
|
|---|
| 303 |
def get_return_data_type(func_name): |
|---|
| 304 |
"""Return a somewhat-helpful data type given a function name""" |
|---|
| 305 |
if func_name.startswith('get_'): |
|---|
| 306 |
if func_name.endswith('_list'): |
|---|
| 307 |
return 'List' |
|---|
| 308 |
elif func_name.endswith('_count'): |
|---|
| 309 |
return 'Integer' |
|---|
| 310 |
return '' |
|---|
| 311 |
|
|---|
| 312 |
# Maps Field objects to their human-readable data types, as strings. |
|---|
| 313 |
# Column-type strings can contain format strings; they'll be interpolated |
|---|
| 314 |
# against the values of Field.__dict__ before being output. |
|---|
| 315 |
# If a column type is set to None, it won't be included in the output. |
|---|
| 316 |
DATA_TYPE_MAPPING = { |
|---|
| 317 |
'AutoField' : _('Integer'), |
|---|
| 318 |
'BooleanField' : _('Boolean (Either True or False)'), |
|---|
| 319 |
'CharField' : _('String (up to %(max_length)s)'), |
|---|
| 320 |
'CommaSeparatedIntegerField': _('Comma-separated integers'), |
|---|
| 321 |
'DateField' : _('Date (without time)'), |
|---|
| 322 |
'DateTimeField' : _('Date (with time)'), |
|---|
| 323 |
'DecimalField' : _('Decimal number'), |
|---|
| 324 |
'EmailField' : _('E-mail address'), |
|---|
| 325 |
'FileField' : _('File path'), |
|---|
| 326 |
'FilePathField' : _('File path'), |
|---|
| 327 |
'FloatField' : _('Floating point number'), |
|---|
| 328 |
'ForeignKey' : _('Integer'), |
|---|
| 329 |
'ImageField' : _('File path'), |
|---|
| 330 |
'IntegerField' : _('Integer'), |
|---|
| 331 |
'IPAddressField' : _('IP address'), |
|---|
| 332 |
'ManyToManyField' : '', |
|---|
| 333 |
'NullBooleanField' : _('Boolean (Either True, False or None)'), |
|---|
| 334 |
'OneToOneField' : _('Relation to parent model'), |
|---|
| 335 |
'PhoneNumberField' : _('Phone number'), |
|---|
| 336 |
'PositiveIntegerField' : _('Integer'), |
|---|
| 337 |
'PositiveSmallIntegerField' : _('Integer'), |
|---|
| 338 |
'SlugField' : _('String (up to %(max_length)s)'), |
|---|
| 339 |
'SmallIntegerField' : _('Integer'), |
|---|
| 340 |
'TextField' : _('Text'), |
|---|
| 341 |
'TimeField' : _('Time'), |
|---|
| 342 |
'URLField' : _('URL'), |
|---|
| 343 |
'USStateField' : _('U.S. state (two uppercase letters)'), |
|---|
| 344 |
'XMLField' : _('XML text'), |
|---|
| 345 |
} |
|---|
| 346 |
|
|---|
| 347 |
def get_readable_field_data_type(field): |
|---|
| 348 |
return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__ |
|---|
| 349 |
|
|---|
| 350 |
def extract_views_from_urlpatterns(urlpatterns, base=''): |
|---|
| 351 |
""" |
|---|
| 352 |
Return a list of views from a list of urlpatterns. |
|---|
| 353 |
|
|---|
| 354 |
Each object in the returned list is a two-tuple: (view_func, regex) |
|---|
| 355 |
""" |
|---|
| 356 |
views = [] |
|---|
| 357 |
for p in urlpatterns: |
|---|
| 358 |
if hasattr(p, '_get_callback'): |
|---|
| 359 |
try: |
|---|
| 360 |
views.append((p._get_callback(), base + p.regex.pattern)) |
|---|
| 361 |
except ViewDoesNotExist: |
|---|
| 362 |
continue |
|---|
| 363 |
elif hasattr(p, '_get_url_patterns'): |
|---|
| 364 |
try: |
|---|
| 365 |
patterns = p.url_patterns |
|---|
| 366 |
except ImportError: |
|---|
| 367 |
continue |
|---|
| 368 |
views.extend(extract_views_from_urlpatterns(patterns, base + p.regex.pattern)) |
|---|
| 369 |
else: |
|---|
| 370 |
raise TypeError, _("%s does not appear to be a urlpattern object") % p |
|---|
| 371 |
return views |
|---|
| 372 |
|
|---|
| 373 |
named_group_matcher = re.compile(r'\(\?P(<\w+>).+?\)') |
|---|
| 374 |
non_named_group_matcher = re.compile(r'\(.*?\)') |
|---|
| 375 |
|
|---|
| 376 |
def simplify_regex(pattern): |
|---|
| 377 |
""" |
|---|
| 378 |
Clean up urlpattern regexes into something somewhat readable by Mere Humans: |
|---|
| 379 |
turns something like "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$" |
|---|
| 380 |
into "<sport_slug>/athletes/<athlete_slug>/" |
|---|
| 381 |
""" |
|---|
| 382 |
# handle named groups first |
|---|
| 383 |
pattern = named_group_matcher.sub(lambda m: m.group(1), pattern) |
|---|
| 384 |
|
|---|
| 385 |
# handle non-named groups |
|---|
| 386 |
pattern = non_named_group_matcher.sub("<var>", pattern) |
|---|
| 387 |
|
|---|
| 388 |
# clean up any outstanding regex-y characters. |
|---|
| 389 |
pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/').replace('\\', '') |
|---|
| 390 |
if not pattern.startswith('/'): |
|---|
| 391 |
pattern = '/' + pattern |
|---|
| 392 |
return pattern |
|---|