views.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. from importlib import import_module
  2. import inspect
  3. import os
  4. import re
  5. import warnings
  6. from django import template
  7. from django.apps import apps
  8. from django.conf import settings
  9. from django.contrib import admin
  10. from django.contrib.admin.views.decorators import staff_member_required
  11. from django.db import models
  12. from django.core.exceptions import ViewDoesNotExist
  13. from django.http import Http404
  14. from django.core import urlresolvers
  15. from django.contrib.admindocs import utils
  16. from django.utils.decorators import method_decorator
  17. from django.utils.deprecation import RemovedInDjango18Warning
  18. from django.utils._os import upath
  19. from django.utils import six
  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. if getattr(settings, 'ADMIN_FOR', None):
  25. warnings.warn('The ADMIN_FOR setting has been removed, you can remove '
  26. 'this setting from your configuration.', RemovedInDjango18Warning,
  27. stacklevel=2)
  28. class BaseAdminDocsView(TemplateView):
  29. """
  30. Base view for admindocs views.
  31. """
  32. @method_decorator(staff_member_required)
  33. def dispatch(self, *args, **kwargs):
  34. if not utils.docutils_is_available:
  35. # Display an error message for people without docutils
  36. self.template_name = 'admin_doc/missing_docutils.html'
  37. return self.render_to_response(admin.site.each_context())
  38. return super(BaseAdminDocsView, self).dispatch(*args, **kwargs)
  39. def get_context_data(self, **kwargs):
  40. kwargs.update({'root_path': urlresolvers.reverse('admin:index')})
  41. kwargs.update(admin.site.each_context())
  42. return super(BaseAdminDocsView, self).get_context_data(**kwargs)
  43. class BookmarkletsView(BaseAdminDocsView):
  44. template_name = 'admin_doc/bookmarklets.html'
  45. def get_context_data(self, **kwargs):
  46. context = super(BookmarkletsView, self).get_context_data(**kwargs)
  47. context.update({
  48. 'admin_url': "%s://%s%s" % (
  49. self.request.scheme, self.request.get_host(), context['root_path'])
  50. })
  51. return context
  52. class TemplateTagIndexView(BaseAdminDocsView):
  53. template_name = 'admin_doc/template_tag_index.html'
  54. def get_context_data(self, **kwargs):
  55. load_all_installed_template_libraries()
  56. tags = []
  57. app_libs = list(six.iteritems(template.libraries))
  58. builtin_libs = [(None, lib) for lib in template.builtins]
  59. for module_name, library in builtin_libs + app_libs:
  60. for tag_name, tag_func in library.tags.items():
  61. title, body, metadata = utils.parse_docstring(tag_func.__doc__)
  62. if title:
  63. title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
  64. if body:
  65. body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
  66. for key in metadata:
  67. metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
  68. if library in template.builtins:
  69. tag_library = ''
  70. else:
  71. tag_library = module_name.split('.')[-1]
  72. tags.append({
  73. 'name': tag_name,
  74. 'title': title,
  75. 'body': body,
  76. 'meta': metadata,
  77. 'library': tag_library,
  78. })
  79. kwargs.update({'tags': tags})
  80. return super(TemplateTagIndexView, self).get_context_data(**kwargs)
  81. class TemplateFilterIndexView(BaseAdminDocsView):
  82. template_name = 'admin_doc/template_filter_index.html'
  83. def get_context_data(self, **kwargs):
  84. load_all_installed_template_libraries()
  85. filters = []
  86. app_libs = list(six.iteritems(template.libraries))
  87. builtin_libs = [(None, lib) for lib in template.builtins]
  88. for module_name, library in builtin_libs + app_libs:
  89. for filter_name, filter_func in library.filters.items():
  90. title, body, metadata = utils.parse_docstring(filter_func.__doc__)
  91. if title:
  92. title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
  93. if body:
  94. body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
  95. for key in metadata:
  96. metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
  97. if library in template.builtins:
  98. tag_library = ''
  99. else:
  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. mod, func = urlresolvers.get_mod_func(view)
  131. try:
  132. view_func = getattr(import_module(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. kwargs.update({
  143. 'name': view,
  144. 'summary': title,
  145. 'body': body,
  146. 'meta': metadata,
  147. })
  148. return super(ViewDetailView, self).get_context_data(**kwargs)
  149. class ModelIndexView(BaseAdminDocsView):
  150. template_name = 'admin_doc/model_index.html'
  151. def get_context_data(self, **kwargs):
  152. m_list = [m._meta for m in apps.get_models()]
  153. kwargs.update({'models': m_list})
  154. return super(ModelIndexView, self).get_context_data(**kwargs)
  155. class ModelDetailView(BaseAdminDocsView):
  156. template_name = 'admin_doc/model_detail.html'
  157. def get_context_data(self, **kwargs):
  158. # Get the model class.
  159. try:
  160. app_config = apps.get_app_config(self.kwargs['app_label'])
  161. except LookupError:
  162. raise Http404(_("App %(app_label)r not found") % self.kwargs)
  163. try:
  164. model = app_config.get_model(self.kwargs['model_name'])
  165. except LookupError:
  166. raise Http404(_("Model %(model_name)r not found in app %(app_label)r") % self.kwargs)
  167. opts = model._meta
  168. # Gather fields/field descriptions.
  169. fields = []
  170. for field in opts.fields:
  171. # ForeignKey is a special case since the field will actually be a
  172. # descriptor that returns the other object
  173. if isinstance(field, models.ForeignKey):
  174. data_type = field.rel.to.__name__
  175. app_label = field.rel.to._meta.app_label
  176. verbose = utils.parse_rst(
  177. (_("the related `%(app_label)s.%(data_type)s` object") % {
  178. 'app_label': app_label, 'data_type': data_type,
  179. }),
  180. 'model',
  181. _('model:') + data_type,
  182. )
  183. else:
  184. data_type = get_readable_field_data_type(field)
  185. verbose = field.verbose_name
  186. fields.append({
  187. 'name': field.name,
  188. 'data_type': data_type,
  189. 'verbose': verbose,
  190. 'help_text': field.help_text,
  191. })
  192. # Gather many-to-many fields.
  193. for field in opts.many_to_many:
  194. data_type = field.rel.to.__name__
  195. app_label = field.rel.to._meta.app_label
  196. verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': app_label, 'object_name': data_type}
  197. fields.append({
  198. 'name': "%s.all" % field.name,
  199. "data_type": 'List',
  200. 'verbose': utils.parse_rst(_("all %s") % verbose, 'model', _('model:') + opts.model_name),
  201. })
  202. fields.append({
  203. 'name': "%s.count" % field.name,
  204. 'data_type': 'Integer',
  205. 'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
  206. })
  207. # Gather model methods.
  208. for func_name, func in model.__dict__.items():
  209. if (inspect.isfunction(func) and len(inspect.getargspec(func)[0]) == 1):
  210. try:
  211. for exclude in MODEL_METHODS_EXCLUDE:
  212. if func_name.startswith(exclude):
  213. raise StopIteration
  214. except StopIteration:
  215. continue
  216. verbose = func.__doc__
  217. if verbose:
  218. verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
  219. fields.append({
  220. 'name': func_name,
  221. 'data_type': get_return_data_type(func_name),
  222. 'verbose': verbose,
  223. })
  224. # Gather related objects
  225. for rel in opts.get_all_related_objects() + opts.get_all_related_many_to_many_objects():
  226. verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name}
  227. accessor = rel.get_accessor_name()
  228. fields.append({
  229. 'name': "%s.all" % accessor,
  230. 'data_type': 'List',
  231. 'verbose': utils.parse_rst(_("all %s") % verbose, 'model', _('model:') + opts.model_name),
  232. })
  233. fields.append({
  234. 'name': "%s.count" % accessor,
  235. 'data_type': 'Integer',
  236. 'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
  237. })
  238. kwargs.update({
  239. 'name': '%s.%s' % (opts.app_label, opts.object_name),
  240. # Translators: %s is an object type name
  241. 'summary': _("Attributes on %s objects") % opts.object_name,
  242. 'description': model.__doc__,
  243. 'fields': fields,
  244. })
  245. return super(ModelDetailView, self).get_context_data(**kwargs)
  246. class TemplateDetailView(BaseAdminDocsView):
  247. template_name = 'admin_doc/template_detail.html'
  248. def get_context_data(self, **kwargs):
  249. template = self.kwargs['template']
  250. templates = []
  251. for dir in settings.TEMPLATE_DIRS:
  252. template_file = os.path.join(dir, template)
  253. templates.append({
  254. 'file': template_file,
  255. 'exists': os.path.exists(template_file),
  256. 'contents': lambda: open(template_file).read() if os.path.exists(template_file) else '',
  257. 'order': list(settings.TEMPLATE_DIRS).index(dir),
  258. })
  259. kwargs.update({
  260. 'name': template,
  261. 'templates': templates,
  262. })
  263. return super(TemplateDetailView, self).get_context_data(**kwargs)
  264. ####################
  265. # Helper functions #
  266. ####################
  267. def load_all_installed_template_libraries():
  268. # Load/register all template tag libraries from installed apps.
  269. for module_name in template.get_templatetags_modules():
  270. mod = import_module(module_name)
  271. try:
  272. libraries = [
  273. os.path.splitext(p)[0]
  274. for p in os.listdir(os.path.dirname(upath(mod.__file__)))
  275. if p.endswith('.py') and p[0].isalpha()
  276. ]
  277. except OSError:
  278. libraries = []
  279. for library_name in libraries:
  280. try:
  281. template.get_library(library_name)
  282. except template.InvalidTemplateLibrary:
  283. pass
  284. def get_return_data_type(func_name):
  285. """Return a somewhat-helpful data type given a function name"""
  286. if func_name.startswith('get_'):
  287. if func_name.endswith('_list'):
  288. return 'List'
  289. elif func_name.endswith('_count'):
  290. return 'Integer'
  291. return ''
  292. def get_readable_field_data_type(field):
  293. """Returns the description for a given field type, if it exists,
  294. Fields' descriptions can contain format strings, which will be interpolated
  295. against the values of field.__dict__ before being output."""
  296. return field.description % field.__dict__
  297. def extract_views_from_urlpatterns(urlpatterns, base='', namespace=None):
  298. """
  299. Return a list of views from a list of urlpatterns.
  300. Each object in the returned list is a two-tuple: (view_func, regex)
  301. """
  302. views = []
  303. for p in urlpatterns:
  304. if hasattr(p, 'url_patterns'):
  305. try:
  306. patterns = p.url_patterns
  307. except ImportError:
  308. continue
  309. views.extend(extract_views_from_urlpatterns(
  310. patterns,
  311. base + p.regex.pattern,
  312. (namespace or []) + (p.namespace and [p.namespace] or [])
  313. ))
  314. elif hasattr(p, 'callback'):
  315. try:
  316. views.append((p.callback, base + p.regex.pattern,
  317. namespace, p.name))
  318. except ViewDoesNotExist:
  319. continue
  320. else:
  321. raise TypeError(_("%s does not appear to be a urlpattern object") % p)
  322. return views
  323. named_group_matcher = re.compile(r'\(\?P(<\w+>).+?\)')
  324. non_named_group_matcher = re.compile(r'\(.*?\)')
  325. def simplify_regex(pattern):
  326. """
  327. Clean up urlpattern regexes into something somewhat readable by Mere Humans:
  328. turns something like "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
  329. into "<sport_slug>/athletes/<athlete_slug>/"
  330. """
  331. # handle named groups first
  332. pattern = named_group_matcher.sub(lambda m: m.group(1), pattern)
  333. # handle non-named groups
  334. pattern = non_named_group_matcher.sub("<var>", pattern)
  335. # clean up any outstanding regex-y characters.
  336. pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/').replace('\\', '')
  337. if not pattern.startswith('/'):
  338. pattern = '/' + pattern
  339. return pattern