You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2013/06/12 15:29:21 UTC

git commit: [#6332] Add api docs for classes extended/used by plugins

Updated Branches:
  refs/heads/tv/6332 [created] 4226de09e


[#6332] Add api docs for classes extended/used by plugins

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/4226de09
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4226de09
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4226de09

Branch: refs/heads/tv/6332
Commit: 4226de09e05d7e51bc7351d55f07204689cb9b1a
Parents: 22dc0fe
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Jun 12 13:29:04 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jun 12 13:29:04 2013 +0000

----------------------------------------------------------------------
 Allura/allura/app.py            | 194 +++++++++++++++++++++++++++++------
 Allura/allura/model/artifact.py | 161 +++++++++++++++++++++++------
 Allura/docs/api/app.rst         |  10 ++
 Allura/docs/api/model.rst       |  28 +++++
 4 files changed, 330 insertions(+), 63 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4226de09/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 21099a1..7ce847d 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -37,22 +37,40 @@ from allura.lib.utils import permanent_redirect
 
 log = logging.getLogger(__name__)
 
+
 class ConfigOption(object):
+    """Definition of a configuration option for an :class:`Application`.
 
+    """
     def __init__(self, name, ming_type, default, label=None):
+        """Create a new ConfigOption.
+
+        """
         self.name, self.ming_type, self._default, self.label = (
             name, ming_type, default, label or name)
 
     @property
     def default(self):
+        """Return the default value for this ConfigOption.
+
+        """
         if callable(self._default):
             return self._default()
         return self._default
 
+
 class SitemapEntry(object):
+    """A labeled URL, which may optionally have
+    :class:`children <SitemapEntry>`.
+
+    Used for generating trees of links.
 
+    """
     def __init__(self, label, url=None, children=None, className=None,
-            ui_icon=None, small=None, tool_name=None):
+            ui_icon=None, small=None, tool_name=None, matching_urls=None):
+        """Create a new SitemapEntry.
+
+        """
         self.label = label
         self.className = className
         if url is not None:
@@ -60,15 +78,14 @@ class SitemapEntry(object):
         self.url = url
         self.small = small
         self.ui_icon = ui_icon
-        if children is None:
-            children = []
-        self.children = children
+        self.children = children or []
         self.tool_name = tool_name
-        self.matching_urls = []
+        self.matching_urls = matching_urls or []
 
     def __getitem__(self, x):
-        """
-        Automatically expand the list of sitemap child entries with the given items.  Example:
+        """Automatically expand the list of sitemap child entries with the
+        given items.  Example::
+
             SitemapEntry('HelloForge')[
                 SitemapEntry('foo')[
                     SitemapEntry('Pages')[pages]
@@ -76,6 +93,7 @@ class SitemapEntry(object):
             ]
 
         TODO: deprecate this; use a more clear method of building a tree
+
         """
         if isinstance(x, (list, tuple)):
             self.children.extend(list(x))
@@ -92,6 +110,12 @@ class SitemapEntry(object):
         return '\n'.join(l)
 
     def bind_app(self, app):
+        """Recreate this SitemapEntry in the context of
+        :class:`app <Application>`.
+
+        :returns: :class:`SitemapEntry`
+
+        """
         lbl = self.label
         url = self.url
         if callable(lbl):
@@ -99,12 +123,26 @@ class SitemapEntry(object):
         if url is not None:
             url = basejoin(app.url, url)
         return SitemapEntry(lbl, url, [
-                ch.bind_app(app) for ch in self.children], className=self.className)
+                ch.bind_app(app) for ch in self.children],
+                className=self.className,
+                ui_icon=self.ui_icon,
+                small=self.small,
+                tool_name=self.tool_name,
+                matching_urls=self.matching_urls)
+
+    def extend(self, sitemap_entries):
+        """Extend our children with ``sitemap_entries``.
+
+        :param sitemap_entries: list of :class:`SitemapEntry`
+
+        For each entry, if it doesn't already exist in our children, add it.
+        If it does already exist in our children, recursively extend the
+        children or our copy with the children of the new copy.
 
-    def extend(self, sitemap):
+        """
         child_index = dict(
             (ch.label, ch) for ch in self.children)
-        for e in sitemap:
+        for e in sitemap_entries:
             lbl = e.label
             match = child_index.get(e.label)
             if match and match.url == e.url:
@@ -114,7 +152,9 @@ class SitemapEntry(object):
                 child_index[lbl] = e
 
     def matches_url(self, request):
-        """Return true if this SitemapEntry 'matches' the url of `request`."""
+        """Return True if this SitemapEntry 'matches' the url of ``request``.
+
+        """
         return self.url in request.upath_info or any([
             url in request.upath_info for url in self.matching_urls])
 
@@ -137,7 +177,7 @@ class Application(object):
     :cvar list permissions: Named permissions used by instances of this
         Application. Default is [].
     :cvar list sitemap: :class:`SitemapEntries <allura.app.SitemapEntry>`
-        used to create the Application's navigation in the left side bar.
+        used to create the Application's navigation in the main project nav.
         Default is [].
     :cvar bool installable: Default is True, Application can be installed in
         projects.
@@ -177,33 +217,53 @@ class Application(object):
     DiscussionClass = model.Discussion
     PostClass = model.Post
     AttachmentClass = model.DiscussionAttachment
-    tool_label='Tool'
-    tool_description="This is a tool for Allura forge."
-    default_mount_label='Tool Name'
-    default_mount_point='tool'
-    relaxed_mount_points=False
-    ordinal=0
+    tool_label = 'Tool'
+    tool_description = "This is a tool for Allura forge."
+    default_mount_label = 'Tool Name'
+    default_mount_point = 'tool'
+    relaxed_mount_points = False
+    ordinal = 0
     hidden = False
-    icons={
+    icons = {
         24:'images/admin_24.png',
         32:'images/admin_32.png',
         48:'images/admin_48.png'
     }
 
     def __init__(self, project, app_config_object):
+        """Create an Application instance.
+
+        :param project: Project to which this Application belongs
+        :type project: :class:`allura.model.project.Project`
+        :param app_config_object: Config describing this Application
+        :type app_config_object: :class:`allura.model.project.AppConfig`
+
+        """
         self.project = project
         self.config = app_config_object
         self.admin = DefaultAdminController(self)
 
     @LazyProperty
     def url(self):
+        """Return the URL for this Application.
+
+        """
         return self.config.url(project=self.project)
 
     @property
     def acl(self):
+        """Return the :class:`Access Control List <allura.model.types.ACL>`
+        for this Application.
+
+        """
         return self.config.acl
 
     def parent_security_context(self):
+        """Return the parent of this object.
+
+        Used for calculating permissions based on trees of ACLs.
+
+        """
         return self.config.parent_security_context()
 
     @classmethod
@@ -226,17 +286,22 @@ class Application(object):
 
     @classmethod
     def status_int(self):
+        """Return the :attr:`status` of this Application as an int.
+
+        Used for sorting available Apps by status in the Admin interface.
+
+        """
         return self.status_map.index(self.status)
 
     @classmethod
     def icon_url(self, size):
-        '''Subclasses (tools) provide their own icons (preferred) or in
-        extraordinary circumstances override this routine to provide
-        the URL to an icon of the requested size specific to that tool.
+        """Return URL for icon of the given ``size``.
+
+        Subclasses can define their own icons by overriding
+        :attr:`icons` or by overriding this method (which, by default,
+        returns the URLs defined in :attr:`icons`).
 
-        Application.icons is simply a default if no more specific icon
-        is available.
-        '''
+        """
         resource = self.icons.get(size)
         if resource:
             return g.theme_href(resource)
@@ -263,6 +328,10 @@ class Application(object):
         return has_access(self, 'read')(user=user)
 
     def subscribe_admins(self):
+        """Subscribe all project Admins (for this Application's project) to the
+        :class:`allura.model.notification.Mailbox` for this Application.
+
+        """
         for uid in g.credentials.userids_with_named_role(self.project._id, 'Admin'):
             model.Mailbox.subscribe(
                 type='direct',
@@ -271,6 +340,10 @@ class Application(object):
                 app_config_id=self.config._id)
 
     def subscribe(self, user):
+        """Subscribe :class:`user <allura.model.auth.User>` to the
+        :class:`allura.model.notification.Mailbox` for this Application.
+
+        """
         if user and user != model.User.anonymous():
             model.Mailbox.subscribe(
                     type='direct',
@@ -322,26 +395,31 @@ class Application(object):
         By default, an app can be uninstalled iff it can be installed, although
         some apps may want/need to override this (e.g. an app which can
         not be installed directly by a user, but may be uninstalled).
+
         """
         return self.installable
 
     def main_menu(self):
-        '''Apps should provide their entries to be added to the main nav
-        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
-        '''
+        """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        to display in the main project nav for this Application.
+
+        Default implementation returns :attr:`sitemap`.
+
+        """
         return self.sitemap
 
     def sidebar_menu(self):
-        """
-        Apps should override this to provide their menu
-        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        to render in the left sidebar for this Application.
+
         """
         return []
 
     def sidebar_menu_js(self):
-        """
-        Apps can override this to provide Javascript needed by the sidebar_menu.
+        """Return Javascript needed by the sidebar menu of this Application.
+
         :return: a string of Javascript code
+
         """
         return ""
 
@@ -388,6 +466,17 @@ class Application(object):
         pass
 
     def handle_artifact_message(self, artifact, message):
+        """Handle message addressed to this Application.
+
+        :param artifact: Specific artifact to which the message is addressed
+        :type artifact: :class:`allura.model.artifact.Artifact`
+        :param message: the message
+        :type message: :class:`allura.model.artifact.Message`
+
+        Default implementation posts the message to the appropriate discussion
+        thread for the artifact.
+
+        """
         # Find ancestor comment and thread
         thd, parent_id = artifact.get_discussion_thread(message)
         # Handle attachments
@@ -423,18 +512,41 @@ class Application(object):
                 text=text,
                 subject=message['headers'].get('Subject', 'no subject'))
 
+
 class DefaultAdminController(BaseController):
+    """Provides basic admin functionality for an :class:`Application`.
+
+    To add more admin functionality for your Application, extend this
+    class and then assign an instance of it to the ``admin`` attr of
+    your Application::
 
+        class MyApp(Application):
+            def __init__(self, *args):
+                super(MyApp, self).__init__(*args)
+                self.admin = MyAdminController(self)
+
+    """
     def __init__(self, app):
+        """Instantiate this controller for an :class:`app <Application>`.
+
+        """
         self.app = app
 
     @expose()
     def index(self, **kw):
+        """Home page for this controller.
+
+        Redirects to the 'permissions' page by default.
+
+        """
         permanent_redirect('permissions')
 
     @expose('jinja:allura:templates/app_admin_permissions.html')
     @without_trailing_slash
     def permissions(self):
+        """Render the permissions management web page.
+
+        """
         from ext.admin.widgets import PermissionCard
         c.card = PermissionCard()
         permissions = dict((p, []) for p in self.app.permissions)
@@ -452,6 +564,9 @@ class DefaultAdminController(BaseController):
 
     @expose('jinja:allura:templates/app_admin_edit_label.html')
     def edit_label(self):
+        """Renders form to update the Application's ``mount_label``.
+
+        """
         return dict(
             app=self.app,
             allow_config=has_access(self.app, 'configure')())
@@ -459,12 +574,18 @@ class DefaultAdminController(BaseController):
     @expose()
     @require_post()
     def update_label(self, mount_label):
+        """Handles POST to update the Application's ``mount_label``.
+
+        """
         require_access(self.app, 'configure')
         self.app.config.options['mount_label'] = mount_label
         redirect(request.referer)
 
     @expose('jinja:allura:templates/app_admin_options.html')
     def options(self):
+        """Renders form to update the Application's ``config.options``.
+
+        """
         return dict(
             app=self.app,
             allow_config=has_access(self.app, 'configure')())
@@ -472,6 +593,10 @@ class DefaultAdminController(BaseController):
     @expose()
     @require_post()
     def configure(self, **kw):
+        """Handle POST to delete the Application or update its
+        ``config.options``.
+
+        """
         with h.push_config(c, app=self.app):
             require_access(self.app, 'configure')
             is_admin = self.app.config.tool_name == 'admin'
@@ -506,6 +631,9 @@ class DefaultAdminController(BaseController):
     @h.vardec
     @require_post()
     def update(self, card=None, **kw):
+        """Handle POST to update permissions for the Application.
+
+        """
         old_acl = self.app.config.acl
         self.app.config.acl = []
         for args in card:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4226de09/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 5aea67f..fefd9b4 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -44,20 +44,20 @@ from filesystem import File
 
 log = logging.getLogger(__name__)
 
+
 class Artifact(MappedClass):
     """
-    The base class for anything you want to keep track of.
+    Base class for anything you want to keep track of.
 
-    It will automatically be added to solr (see index() method).  It also
-    gains a discussion thread and can have files attached to it.
+    - Automatically indexed into Solr (see index() method)
+    - Has a discussion thread that can have files attached to it
 
-    :var tool_version: default's to the app's version
+    :var mod_date: last-modified :class:`datetime`
+    :var tool_version: defaults to the parent Application's version
     :var acl: dict of permission name => [roles]
     :var labels: list of plain old strings
-    :var references: list of outgoing references to other tickets
-    :var backreferences: dict of incoming references to this artifact, mapped by solr id
-    """
 
+    """
     class __mongometa__:
         session = artifact_orm_session
         name='artifact'
@@ -90,6 +90,10 @@ class Artifact(MappedClass):
     deleted=FieldProperty(bool, if_missing=False)
 
     def __json__(self):
+        """Return a JSON-encodable :class:`dict` representation of this
+        Artifact.
+
+        """
         return dict(
             _id=str(self._id),
             mod_date=self.mod_date,
@@ -100,8 +104,14 @@ class Artifact(MappedClass):
         )
 
     def parent_security_context(self):
-        '''ACL processing should continue at the  AppConfig object. This lets
-        AppConfigs provide a 'default' ACL for all artifacts in the tool.'''
+        """Return the :class:`allura.model.project.AppConfig` instance for
+        this Artifact.
+
+        ACL processing for this Artifact continues at the AppConfig object.
+        This lets AppConfigs provide a 'default' ACL for all artifacts in the
+        tool.
+
+        """
         return self.app_config
 
     @classmethod
@@ -110,6 +120,11 @@ class Artifact(MappedClass):
 
     @classmethod
     def translate_query(cls, q, fields):
+        """Return a translated Solr query (``q``), where generic field
+        identifiers are replaced by the 'strongly typed' versions defined in
+        ``fields``.
+
+        """
         for f in fields:
             if '_' in f:
                 base, typ = f.rsplit('_', 1)
@@ -118,18 +133,34 @@ class Artifact(MappedClass):
 
     @LazyProperty
     def ref(self):
+        """Return :class:`allura.model.index.ArtifactReference` for this
+        Artifact.
+
+        """
         return ArtifactReference.from_artifact(self)
 
     @LazyProperty
     def refs(self):
+        """Artifacts referenced by this one.
+
+        :return: list of :class:`allura.model.index.ArtifactReference`
+        """
         return self.ref.references
 
     @LazyProperty
     def backrefs(self):
+        """Artifacts that reference this one.
+
+        :return: list of :attr:`allura.model.index.ArtifactReference._id`'s
+
+        """
         q = ArtifactReference.query.find(dict(references=self.index_id()))
         return [ aref._id for aref in q ]
 
     def related_artifacts(self):
+        """Return all Artifacts that are related to this one.
+
+        """
         related_artifacts = []
         for ref_id in self.refs+self.backrefs:
             ref = ArtifactReference.query.get(_id=ref_id)
@@ -140,6 +171,8 @@ class Artifact(MappedClass):
             # don't link to artifacts in deleted tools
             if hasattr(artifact, 'app_config') and artifact.app_config is None:
                 continue
+            # TODO: This should be refactored. We shouldn't be checking
+            # artifact type strings in platform code.
             if artifact.type_s == 'Commit' and not artifact.repo:
                 ac = AppConfig.query.get(
                         _id=ref.artifact_reference['app_config_id'])
@@ -151,6 +184,14 @@ class Artifact(MappedClass):
         return related_artifacts
 
     def subscribe(self, user=None, topic=None, type='direct', n=1, unit='day'):
+        """Subscribe ``user`` to the :class:`allura.model.notification.Mailbox`
+        for this Artifact.
+
+        :param user: :class:`allura.model.auth.User`
+
+        If ``user`` is None, ``c.user`` will be subscribed.
+
+        """
         from allura.model import Mailbox
         if user is None: user = c.user
         Mailbox.subscribe(
@@ -161,6 +202,14 @@ class Artifact(MappedClass):
             type=type, n=n, unit=unit)
 
     def unsubscribe(self, user=None):
+        """Unsubscribe ``user`` from the
+        :class:`allura.model.notification.Mailbox` for this Artifact.
+
+        :param user: :class:`allura.model.auth.User`
+
+        If ``user`` is None, ``c.user`` will be unsubscribed.
+
+        """
         from allura.model import Mailbox
         if user is None: user = c.user
         Mailbox.unsubscribe(
@@ -170,19 +219,27 @@ class Artifact(MappedClass):
             artifact_index_id=self.index_id())
 
     def primary(self):
-        '''If an artifact is a "secondary" artifact (discussion of a ticket, for
+        """If an artifact is a "secondary" artifact (discussion of a ticket, for
         instance), return the artifact that is the "primary".
-        '''
+
+        """
         return self
 
     @classmethod
     def artifacts_labeled_with(cls, label, app_config):
-        """Return all artifacts of type `cls` that have the label `label` and
-        are in the tool denoted by `app_config`.
+        """Return all artifacts of type ``cls`` that have the label ``label`` and
+        are in the tool denoted by ``app_config``.
+
+        :param label: str
+        :param app_config: :class:`allura.model.project.AppConfig` instance
+
         """
         return cls.query.find({'labels':label, 'app_config_id': app_config._id})
 
     def email_link(self, subject='artifact'):
+        """Return a 'mailto' URL for this Artifact, with optional subject.
+
+        """
         if subject:
             return 'mailto:%s?subject=[%s:%s:%s] Re: %s' % (
                 self.email_address,
@@ -195,14 +252,26 @@ class Artifact(MappedClass):
 
     @property
     def project(self):
+        """Return the :class:`allura.model.project.Project` instance to which
+        this Artifact belongs.
+
+        """
         return self.app_config.project
 
     @property
     def project_id(self):
+        """Return the ``_id`` of the :class:`allura.model.project.Project`
+        instance to which this Artifact belongs.
+
+        """
         return self.app_config.project_id
 
     @LazyProperty
     def app(self):
+        """Return the :class:`allura.model.app.Application` instance to which
+        this Artifact belongs.
+
+        """
         if not self.app_config:
             return None
         if getattr(c, 'app', None) and c.app.config._id == self.app_config._id:
@@ -211,9 +280,11 @@ class Artifact(MappedClass):
             return self.app_config.load()(self.project, self.app_config)
 
     def index_id(self):
-        '''Globally unique artifact identifier.  Used for
-        SOLR ID, shortlinks, and maybe elsewhere
-        '''
+        """Return a globally unique artifact identifier.
+
+        Used for SOLR ID, shortlinks, and possibly elsewhere.
+
+        """
         id = '%s.%s#%s' % (
             self.__class__.__module__,
             self.__class__.__name__,
@@ -221,17 +292,25 @@ class Artifact(MappedClass):
         return id.replace('.', '/')
 
     def index(self):
-        """
+        """Return a :class:`dict` representation of this Artifact suitable for
+        search indexing.
+
         Subclasses should override this, providing a dictionary of solr_field => value.
-        These fields & values will be stored by solr.  Subclasses should call the
+        These fields & values will be stored by Solr.  Subclasses should call the
         super() index() and then extend it with more fields.
 
-        The _s and _t suffixes, for example, follow solr dynamic field naming
-        pattern.
         You probably want to override at least title and text to have
         meaningful search results and email senders.
-        """
 
+        You can take advantage of Solr's dynamic field typing by adding a type
+        suffix to your field names, e.g.:
+
+            _s (string) (not analyzed)
+            _t (text) (analyzed)
+            _b (bool)
+            _i (int)
+
+        """
         project = self.project
         return dict(
             id=self.index_id(),
@@ -250,32 +329,39 @@ class Artifact(MappedClass):
             deleted_b=self.deleted)
 
     def url(self):
-        """
-        Subclasses should implement this, providing the URL to the artifact
+        """Return the URL for this Artifact.
+
+        Subclasses must implement this.
+
         """
         raise NotImplementedError, 'url' # pragma no cover
 
     def shorthand_id(self):
-        '''How to refer to this artifact within the app instance context.
+        """How to refer to this artifact within the app instance context.
 
         For a wiki page, it might be the title.  For a ticket, it might be the
         ticket number.  For a discussion, it might be the message ID.  Generally
         this should have a strong correlation to the URL.
-        '''
+
+        """
         return str(self._id) # pragma no cover
 
     def link_text(self):
-        '''The link text that will be used when a shortlink to this artifact
+        """Return the link text to use when a shortlink to this artifact
         is expanded into an <a></a> tag.
 
-        By default this method returns shorthand_id(). Subclasses should
+        By default this method returns :meth:`shorthand_id`. Subclasses should
         override this method to provide more descriptive link text.
-        '''
+
+        """
         return self.shorthand_id()
 
     def get_discussion_thread(self, data=None):
-        '''Return the discussion thread for this artifact (possibly made more
-        specific by the message_data)'''
+        """Return the discussion thread and parent_id for this artifact.
+
+        :return: (:class:`allura.model.discuss.Thread`, parent_thread_id (int))
+
+        """
         from .discuss import Thread
         t = Thread.query.get(ref_id=self.index_id())
         if t is None:
@@ -293,18 +379,33 @@ class Artifact(MappedClass):
 
     @LazyProperty
     def discussion_thread(self):
+        """Return the :class:`discussion thread <allura.model.discuss.Thread>`
+        for this Artifact.
+
+        """
         return self.get_discussion_thread()[0]
 
     def attach(self, filename, fp, **kw):
+        """Attach a file to this Artifact.
+
+        :param filename: file name
+        :param fp: a file-like object (implements ``read()``)
+        :param \*\*kw: passed through to Attachment class constructor
+
+        """
         att = self.attachment_class().save_attachment(
             filename=filename,
             fp=fp, artifact_id=self._id, **kw)
         return att
 
     def delete(self):
+        """Delete this Artifact.
+
+        """
         ArtifactReference.query.remove(dict(_id=self.index_id()))
         super(Artifact, self).delete()
 
+
 class Snapshot(Artifact):
     """A snapshot of an :class:`Artifact <allura.model.artifact.Artifact>`, used in :class:`VersionedArtifact <allura.model.artifact.VersionedArtifact>`"""
     class __mongometa__:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4226de09/Allura/docs/api/app.rst
----------------------------------------------------------------------
diff --git a/Allura/docs/api/app.rst b/Allura/docs/api/app.rst
index 31a94f2..491d0b4 100644
--- a/Allura/docs/api/app.rst
+++ b/Allura/docs/api/app.rst
@@ -24,3 +24,13 @@
 
   .. autoclass:: Application
       :members:
+
+  .. autoclass:: ConfigOption
+      :members:
+
+  .. autoclass:: DefaultAdminController
+      :members:
+
+  .. autoclass:: SitemapEntry
+      :members:
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4226de09/Allura/docs/api/model.rst
----------------------------------------------------------------------
diff --git a/Allura/docs/api/model.rst b/Allura/docs/api/model.rst
new file mode 100644
index 0000000..2d30d1d
--- /dev/null
+++ b/Allura/docs/api/model.rst
@@ -0,0 +1,28 @@
+..     Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+
+.. _model_module:
+
+:mod:`allura.model`
+--------------------------------
+
+.. automodule:: allura.model
+
+  .. automodule:: allura.model.artifact
+
+    .. autoclass:: Artifact
+        :members: