--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -37,22 +37,40 @@
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 @@
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 @@
]
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 @@
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 @@
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)
-
- def extend(self, sitemap):
+ 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.
+
+ """
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 @@
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 @@
: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 @@
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 @@
@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.
-
- Application.icons is simply a default if no more specific icon
- is available.
- '''
+ """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`).
+
+ """
resource = self.icons.get(size)
if resource:
return g.theme_href(resource)
@@ -263,6 +328,10 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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: