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: