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