Commit f3dc1732 authored by Zan Anderle's avatar Zan Anderle Committed by Tim Graham
Browse files

Fixed #24917 -- Made admindocs display model methods that take arguments.

parent 3c5862cc
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@

{{ description }}

<h3>{% trans 'Fields' %}</h3>
<div class="module">
<table class="model">
<thead>
@@ -48,6 +49,30 @@
</table>
</div>

{% if methods %}
<h3>{% trans 'Methods with arguments' %}</h3>
<div class="module">
<table class="model">
<thead>
<tr>
    <th>{% trans 'Method' %}</th>
    <th>{% trans 'Arguments' %}</th>
    <th>{% trans 'Description' %}</th>
</tr>
</thead>
<tbody>
{% for method in methods|dictsort:"name" %}
<tr>
    <td>{{ method.name }}</td>
    <td>{{ method.arguments }}</td>
    <td>{{ method.verbose }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

<p class="small"><a href="{% url 'django-admindocs-models-index' %}">&lsaquo; {% trans 'Back to Model Documentation' %}</a></p>
</div>
{% endblock %}
+31 −8
Original line number Diff line number Diff line
@@ -14,7 +14,10 @@ from django.db import models
from django.http import Http404
from django.template.engine import Engine
from django.utils.decorators import method_decorator
from django.utils.inspect import func_has_no_args
from django.utils.inspect import (
    func_accepts_kwargs, func_accepts_var_args, func_has_no_args,
    get_func_full_args,
)
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView

@@ -219,7 +222,7 @@ class ModelDetailView(BaseAdminDocsView):
            fields.append({
                'name': field.name,
                'data_type': data_type,
                'verbose': verbose,
                'verbose': verbose or '',
                'help_text': field.help_text,
            })

@@ -242,9 +245,10 @@ class ModelDetailView(BaseAdminDocsView):
                'verbose': utils.parse_rst(_("number of %s") % verbose, 'model', _('model:') + opts.model_name),
            })

        methods = []
        # Gather model methods.
        for func_name, func in model.__dict__.items():
            if inspect.isfunction(func) and func_has_no_args(func):
            if inspect.isfunction(func):
                try:
                    for exclude in MODEL_METHODS_EXCLUDE:
                        if func_name.startswith(exclude):
@@ -254,10 +258,28 @@ class ModelDetailView(BaseAdminDocsView):
                verbose = func.__doc__
                if verbose:
                    verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name)
                # If a method has no arguments, show it as a 'field', otherwise
                # as a 'method with arguments'.
                if func_has_no_args(func) and not func_accepts_kwargs(func) and not func_accepts_var_args(func):
                    fields.append({
                        'name': func_name,
                        'data_type': get_return_data_type(func_name),
                    'verbose': verbose,
                        'verbose': verbose or '',
                    })
                else:
                    arguments = get_func_full_args(func)
                    print_arguments = arguments
                    # Join arguments with ', ' and in case of default value,
                    # join it with '='. Use repr() so that strings will be
                    # correctly displayed.
                    print_arguments = ', '.join([
                        '='.join(list(arg_el[:1]) + [repr(el) for el in arg_el[1:]])
                        for arg_el in arguments
                    ])
                    methods.append({
                        'name': func_name,
                        'arguments': print_arguments,
                        'verbose': verbose or '',
                    })

        # Gather related objects
@@ -282,6 +304,7 @@ class ModelDetailView(BaseAdminDocsView):
            'summary': title,
            'description': body,
            'fields': fields,
            'methods': methods,
        })
        return super(ModelDetailView, self).get_context_data(**kwargs)

+52 −1
Original line number Diff line number Diff line
@@ -43,6 +43,44 @@ def get_func_args(func):
    ]


def get_func_full_args(func):
    """
    Return a list of (argument name, default value) tuples. If the argument
    does not have a default value, omit it in the tuple. Arguments such as
    *args and **kwargs are also included.
    """
    if six.PY2:
        argspec = inspect.getargspec(func)
        args = argspec.args[1:]  # ignore 'self'
        defaults = argspec.defaults or []
        # Split args into two lists depending on whether they have default value
        no_default = args[:len(args) - len(defaults)]
        with_default = args[len(args) - len(defaults):]
        # Join the two lists and combine it with default values
        args = [(arg,) for arg in no_default] + zip(with_default, defaults)
        # Add possible *args and **kwargs and prepend them with '*' or '**'
        varargs = [('*' + argspec.varargs,)] if argspec.varargs else []
        kwargs = [('**' + argspec.keywords,)] if argspec.keywords else []
        return args + varargs + kwargs

    sig = inspect.signature(func)
    args = []
    for arg_name, param in sig.parameters.items():
        name = arg_name
        # Ignore 'self'
        if name == 'self':
            continue
        if param.kind == inspect.Parameter.VAR_POSITIONAL:
            name = '*' + name
        elif param.kind == inspect.Parameter.VAR_KEYWORD:
            name = '**' + name
        if param.default != inspect.Parameter.empty:
            args.append((name, param.default))
        else:
            args.append((name,))
    return args


def func_accepts_kwargs(func):
    if six.PY2:
        # Not all callables are inspectable with getargspec, so we'll
@@ -64,10 +102,23 @@ def func_accepts_kwargs(func):
    )


def func_accepts_var_args(func):
    """
    Return True if function 'func' accepts positional arguments *args.
    """
    if six.PY2:
        return inspect.getargspec(func)[1] is not None

    return any(
        p for p in inspect.signature(func).parameters.values()
        if p.kind == p.VAR_POSITIONAL
    )


def func_has_no_args(func):
    args = inspect.getargspec(func)[0] if six.PY2 else [
        p for p in inspect.signature(func).parameters.values()
        if p.kind == p.POSITIONAL_OR_KEYWORD and p.default is p.empty
        if p.kind == p.POSITIONAL_OR_KEYWORD
    ]
    return len(args) == 1

+9 −4
Original line number Diff line number Diff line
@@ -60,10 +60,15 @@ Model reference
===============

The **models** section of the ``admindocs`` page describes each model in the
system along with all the fields and methods (without any arguments) available
on it. While model properties don't have any arguments, they are not listed.
Relationships to other models appear as hyperlinks. Descriptions are pulled
from ``help_text`` attributes on fields or from docstrings on model methods.
system along with all the fields and methods available on it. Relationships
to other models appear as hyperlinks. Descriptions are pulled from ``help_text``
attributes on fields or from docstrings on model methods.

.. versionchanged:: 1.9

    The **models** section of the ``admindocs`` now describes methods that take
    arguments as well. In previous versions it was restricted to methods
    without arguments.

A model with useful documentation might look like this::

+6 −0
Original line number Diff line number Diff line
@@ -163,6 +163,12 @@ Minor features

* JavaScript slug generation now supports Romanian characters.

:mod:`django.contrib.admindocs`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* The model section of the ``admindocs`` now also describes methods that take
  arguments, rather than ignoring them.

:mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^

Loading