You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

412 lines
16 KiB

  1. import inspect
  2. import os
  3. import re
  4. from importlib import import_module
  5. from django.apps import apps
  6. from django.conf import settings
  7. from django.contrib import admin
  8. from django.contrib.admin.views.decorators import staff_member_required
  9. from django.contrib.admindocs import utils
  10. from django.core import urlresolvers
  11. from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
  12. from django.db import models
  13. from django.http import Http404
  14. from django.template.engine import Engine
  15. from django.utils.decorators import method_decorator
  16. from django.utils.inspect import (
  17. func_accepts_kwargs, func_accepts_var_args, func_has_no_args,
  18. get_func_full_args,
  19. )
  20. from django.utils.translation import ugettext as _
  21. from django.views.generic import TemplateView
  22. # Exclude methods starting with these strings from documentation
  23. MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_')
  24. class BaseAdminDocsView(TemplateView):
  25. """
  26. Base view for admindocs views.
  27. """
  28. @method_decorator(staff_member_required)
  29. def dispatch(self, request, *args, **kwargs):
  30. if not utils.docutils_is_available:
  31. # Display an error message for people without docutils
  32. self.template_name = 'admin_doc/missing_docutils.html'
  33. return self.render_to_response(admin.site.each_context(request))
  34. return super(BaseAdminDocsView, self).dispatch(request, *args, **kwargs)
  35. def get_context_data(self, **kwargs):
  36. kwargs.update({'root_path': urlresolvers.reverse('admin:index')})
  37. kwargs.update(admin.site.each_context(self.request))
  38. return super(BaseAdminDocsView, self).get_context_data(**kwargs)
  39. class BookmarkletsView(BaseAdminDocsView):
  40. template_name = 'admin_doc/bookmarklets.html'
  41. def get_context_data(self, **kwargs):
  42. context = super(BookmarkletsView, self).get_context_data(**kwargs)
  43. context.update({
  44. 'admin_url': "%s://%s%s" % (
  45. self.request.scheme, self.request.get_host(), context['root_path'])
  46. })
  47. return context
  48. class TemplateTagIndexView(BaseAdminDocsView):
  49. template_name = 'admin_doc/template_tag_index.html'
  50. def get_context_data(self, **kwargs):
  51. tags = []
  52. try:
  53. engine = Engine.get_default()
  54. except ImproperlyConfigured:
  55. # Non-trivial TEMPLATES settings aren't supported (#24125).
  56. pass
  57. else:
  58. app_libs = sorted(engine.template_libraries.items())
  59. builtin_libs = [('', lib) for lib in engine.template_builtins]
  60. for module_name, library in builtin_libs + app_libs:
  61. for tag_name, tag_func in library.tags.items():
  62. title, body, metadata = utils.parse_docstring(tag_func.__doc__)
  63. if title:
  64. title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
  65. if body:
  66. body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
  67. for key in metadata:
  68. metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
  69. tag_library = module_name.split('.')[-1]
  70. tags.append({
  71. 'name': tag_name,
  72. 'title': title,
  73. 'body': body,
  74. 'meta': metadata,
  75. 'library': tag_library,
  76. })
  77. kwargs.update({'tags': tags})
  78. return super(TemplateTagIndexView, self).get_context_data(**kwargs)
  79. class TemplateFilterIndexView(BaseAdminDocsView):
  80. template_name = 'admin_doc/template_filter_index.html'
  81. def get_context_data(self, **kwargs):
  82. filters = []
  83. try:
  84. engine = Engine.get_default()
  85. except ImproperlyConfigured:
  86. # Non-trivial TEMPLATES settings aren't supported (#24125).
  87. pass
  88. else:
  89. app_libs = sorted(engine.template_libraries.items())
  90. builtin_libs = [('', lib) for lib in engine.template_builtins]
  91. for module_name, library in builtin_libs + app_libs:
  92. for filter_name, filter_func in library.filters.items():
  93. title, body, metadata = utils.parse_docstring(filter_func.__doc__)
  94. if title:
  95. title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
  96. if body:
  97. body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
  98. for key in metadata:
  99. metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
  100. tag_library = module_name.split('.')[-1]
  101. filters.append({
  102. 'name': filter_name,
  103. 'title': title,
  104. 'body': body,
  105. 'meta': metadata,
  106. 'library': tag_library,
  107. })
  108. kwargs.update({'filters': filters})
  109. return super(TemplateFilterIndexView, self).get_context_data(**kwargs)
  110. class ViewIndexView(BaseAdminDocsView):
  111. template_name = 'admin_doc/view_index.html'
  112. def get_context_data(self, **kwargs):
  113. views = []
  114. urlconf = import_module(settings.ROOT_URLCONF)
  115. view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns)
  116. for (func, regex, namespace, name) in view_functions:
  117. views.append({
  118. 'full_name': '%s.%s' % (func.__module__, getattr(func, '__name__', func.__class__.__name__)),
  119. 'url': simplify_regex(regex),
  120. 'url_name': ':'.join((namespace or []) + (name and [name] or [])),
  121. 'namespace': ':'.join((namespace or [])),
  122. 'name': name,
  123. })
  124. kwargs.update({'views': views})
  125. return super(ViewIndexView, self).get_context_data(**kwargs)
  126. class ViewDetailView(BaseAdminDocsView):
  127. template_name = 'admin_doc/view_detail.html'
  128. def get_context_data(self, **kwargs):
  129. view = self.kwargs['view']
  130. urlconf = urlresolvers.get_urlconf()
  131. if urlresolvers.get_resolver(urlconf)._is_callback(view):
  132. mod, func = urlresolvers.get_mod_func(view)
  133. view_func = getattr(import_module(mod), func)
  134. else:
  135. raise Http404
  136. title, body, metadata = utils.parse_docstring(view_func.__doc__)
  137. if title:
  138. title = utils.parse_rst(title, 'view', _('view:') + view)
  139. if body:
  140. body = utils.parse_rst(body, 'view', _('view:') + view)
  141. for key in metadata:
  142. metadata[key] = utils.parse_rst(metadata[key], 'model', _('view:') + view)
  143. kwargs.update({
  144. 'name': view,
  145. 'summary': title,
  146. 'body': body,
  147. 'meta': metadata,
  148. })
  149. return super(ViewDetailView, self).get_context_data(**kwargs)
  150. class ModelIndexView(BaseAdminDocsView):
  151. template_name = 'admin_doc/model_index.html'
  152. def get_context_data(self, **kwargs):
  153. m_list = [m._meta for m in apps.get_models()]
  154. kwargs.update({'models': m_list})
  155. return super(ModelIndexView, self).get_context_data(**kwargs)
  156. class ModelDetailView(BaseAdminDocsView):
  157. template_name = 'admin_doc/model_detail.html'
  158. def get_context_data(self, **kwargs):
  159. model_name = self.kwargs['model_name']
  160. # Get the model class.
  161. try:
  162. app_config = apps.get_app_config(self.kwargs['app_label'])
  163. except LookupError:
  164. raise Http404(_("App %(app_label)r not found") % self.kwargs)
  165. try:
  166. model = app_config.get_model(model_name)
  167. except LookupError:
  168. raise Http404(_("Model %(model_name)r not found in app %(app_label)r") % self.kwargs)
  169. opts = model._meta
  170. title, body, metadata = utils.parse_docstring(model.__doc__)
  171. if title:
  172. title = utils.parse_rst(title, 'model', _('model:') + model_name)
  173. if body:
  174. body = utils.parse_rst(body, 'model', _('model:') + model_name)
  175. # Gather fields/field descriptions.
  176. fields = []
  177. for field in opts.fields:
  178. # ForeignKey is a special case since the field will actually be a
  179. # descriptor that returns the other object
  180. if isinstance(field, models.ForeignKey):
  181. data_type = field.remote_field.model.__name__
  182. app_label = field.remote_field.model._meta.app_label
  183. verbose = utils.parse_rst(
  184. (_("the related `%(app_label)s.%(data_type)s` object") % {
  185. 'app_label': app_label, 'data_type': data_type,
  186. }),
  187. 'model',
  188. _('model:') + data_type,
  189. )
  190. else:
  191. data_type = get_readable_field_data_type(field)
  192. verbose = field.verbose_name
  193. fields.append({
  194. 'name': field.name,
  195. 'data_type': data_type,
  196. 'verbose': verbose or '',
  197. 'help_text': field.help_text,
  198. })
  199. # Gather many-to-many fields.
  200. for field in opts.many_to_many:
  201. data_type = field.remote_field.model.__name__
  202. app_label = field.remote_field.model._meta.app_label
  203. verbose = _("related `%(app_label)s.%(object_name)s` objects") % {
  204. 'app_label': app_label,
  205. 'object_name': data_type,
  206. }
  207. fields.append({
  208. 'name': "%s.all" % field.name,
  209. "data_type": 'List',
  210. 'verbose': utils.parse_rst(_("all %s") % verbose, 'model', _('model:') + opts.model_name),
  211. })
  212. fields.append({
  213. 'name': "%s.count" % field.name,
  214. 'data_type': 'Integer',
  215. 'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
  216. })
  217. methods = []
  218. # Gather model methods.
  219. for func_name, func in model.__dict__.items():
  220. if inspect.isfunction(func):
  221. try:
  222. for exclude in MODEL_METHODS_EXCLUDE:
  223. if func_name.startswith(exclude):
  224. raise StopIteration
  225. except StopIteration:
  226. continue
  227. verbose = func.__doc__
  228. if verbose:
  229. verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
  230. # If a method has no arguments, show it as a 'field', otherwise
  231. # as a 'method with arguments'.
  232. if func_has_no_args(func) and not func_accepts_kwargs(func) and not func_accepts_var_args(func):
  233. fields.append({
  234. 'name': func_name,
  235. 'data_type': get_return_data_type(func_name),
  236. 'verbose': verbose or '',
  237. })
  238. else:
  239. arguments = get_func_full_args(func)
  240. print_arguments = arguments
  241. # Join arguments with ', ' and in case of default value,
  242. # join it with '='. Use repr() so that strings will be
  243. # correctly displayed.
  244. print_arguments = ', '.join([
  245. '='.join(list(arg_el[:1]) + [repr(el) for el in arg_el[1:]])
  246. for arg_el in arguments
  247. ])
  248. methods.append({
  249. 'name': func_name,
  250. 'arguments': print_arguments,
  251. 'verbose': verbose or '',
  252. })
  253. # Gather related objects
  254. for rel in opts.related_objects:
  255. verbose = _("related `%(app_label)s.%(object_name)s` objects") % {
  256. 'app_label': rel.related_model._meta.app_label,
  257. 'object_name': rel.related_model._meta.object_name,
  258. }
  259. accessor = rel.get_accessor_name()
  260. fields.append({
  261. 'name': "%s.all" % accessor,
  262. 'data_type': 'List',
  263. 'verbose': utils.parse_rst(_("all %s") % verbose, 'model', _('model:') + opts.model_name),
  264. })
  265. fields.append({
  266. 'name': "%s.count" % accessor,
  267. 'data_type': 'Integer',
  268. 'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
  269. })
  270. kwargs.update({
  271. 'name': '%s.%s' % (opts.app_label, opts.object_name),
  272. 'summary': title,
  273. 'description': body,
  274. 'fields': fields,
  275. 'methods': methods,
  276. })
  277. return super(ModelDetailView, self).get_context_data(**kwargs)
  278. class TemplateDetailView(BaseAdminDocsView):
  279. template_name = 'admin_doc/template_detail.html'
  280. def get_context_data(self, **kwargs):
  281. template = self.kwargs['template']
  282. templates = []
  283. try:
  284. default_engine = Engine.get_default()
  285. except ImproperlyConfigured:
  286. # Non-trivial TEMPLATES settings aren't supported (#24125).
  287. pass
  288. else:
  289. # This doesn't account for template loaders (#24128).
  290. for index, directory in enumerate(default_engine.dirs):
  291. template_file = os.path.join(directory, template)
  292. templates.append({
  293. 'file': template_file,
  294. 'exists': os.path.exists(template_file),
  295. 'contents': lambda: open(template_file).read() if os.path.exists(template_file) else '',
  296. 'order': index,
  297. })
  298. kwargs.update({
  299. 'name': template,
  300. 'templates': templates,
  301. })
  302. return super(TemplateDetailView, self).get_context_data(**kwargs)
  303. ####################
  304. # Helper functions #
  305. ####################
  306. def get_return_data_type(func_name):
  307. """Return a somewhat-helpful data type given a function name"""
  308. if func_name.startswith('get_'):
  309. if func_name.endswith('_list'):
  310. return 'List'
  311. elif func_name.endswith('_count'):
  312. return 'Integer'
  313. return ''
  314. def get_readable_field_data_type(field):
  315. """Returns the description for a given field type, if it exists,
  316. Fields' descriptions can contain format strings, which will be interpolated
  317. against the values of field.__dict__ before being output."""
  318. return field.description % field.__dict__
  319. def extract_views_from_urlpatterns(urlpatterns, base='', namespace=None):
  320. """
  321. Return a list of views from a list of urlpatterns.
  322. Each object in the returned list is a two-tuple: (view_func, regex)
  323. """
  324. views = []
  325. for p in urlpatterns:
  326. if hasattr(p, 'url_patterns'):
  327. try:
  328. patterns = p.url_patterns
  329. except ImportError:
  330. continue
  331. views.extend(extract_views_from_urlpatterns(
  332. patterns,
  333. base + p.regex.pattern,
  334. (namespace or []) + (p.namespace and [p.namespace] or [])
  335. ))
  336. elif hasattr(p, 'callback'):
  337. try:
  338. views.append((p.callback, base + p.regex.pattern,
  339. namespace, p.name))
  340. except ViewDoesNotExist:
  341. continue
  342. else:
  343. raise TypeError(_("%s does not appear to be a urlpattern object") % p)
  344. return views
  345. named_group_matcher = re.compile(r'\(\?P(<\w+>).+?\)')
  346. non_named_group_matcher = re.compile(r'\(.*?\)')
  347. def simplify_regex(pattern):
  348. """
  349. Clean up urlpattern regexes into something somewhat readable by Mere Humans:
  350. turns something like "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
  351. into "<sport_slug>/athletes/<athlete_slug>/"
  352. """
  353. # handle named groups first
  354. pattern = named_group_matcher.sub(lambda m: m.group(1), pattern)
  355. # handle non-named groups
  356. pattern = non_named_group_matcher.sub("<var>", pattern)
  357. # clean up any outstanding regex-y characters.
  358. pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/').replace('\\', '')
  359. if not pattern.startswith('/'):
  360. pattern = '/' + pattern
  361. return pattern