You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by he...@apache.org on 2015/05/29 22:40:26 UTC

[04/45] allura git commit: [#7878] Used 2to3 to see what issues would come up

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/webhooks.py
----------------------------------------------------------------------
diff --git a/webhooks.py b/webhooks.py
new file mode 100644
index 0000000..06971c0
--- /dev/null
+++ b/webhooks.py
@@ -0,0 +1,493 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       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.
+
+import logging
+import json
+import hmac
+import hashlib
+import time
+import socket
+import ssl
+
+import requests
+from bson import ObjectId
+from tg import expose, validate, redirect, flash, config
+from tg.decorators import with_trailing_slash, without_trailing_slash
+from pylons import tmpl_context as c
+from pylons import response, request
+from formencode import validators as fev, schema, Invalid
+from ming.odm import session
+from webob import exc
+from pymongo.errors import DuplicateKeyError
+from paste.deploy.converters import asint, aslist
+
+from .allura.controllers import BaseController
+from .allura.lib import helpers as h
+from .allura.lib.decorators import require_post, task
+from .allura.lib.utils import DateJSONEncoder
+from .allura import model as M
+
+
+log = logging.getLogger(__name__)
+
+
+class WebhookValidator(fev.FancyValidator):
+    def __init__(self, sender, app, **kw):
+        self.app = app
+        self.sender = sender
+        super(WebhookValidator, self).__init__(**kw)
+
+    def _to_python(self, value, state):
+        wh = None
+        if isinstance(value, M.Webhook):
+            wh = value
+        elif isinstance(value, ObjectId):
+            wh = M.Webhook.query.get(_id=value)
+        else:
+            try:
+                wh = M.Webhook.query.get(_id=ObjectId(value))
+            except:
+                pass
+        if wh and wh.type == self.sender.type and wh.app_config_id == self.app.config._id:
+            return wh
+        raise Invalid('Invalid webhook', value, state)
+
+
+class WebhookCreateForm(schema.Schema):
+    url = fev.URL(not_empty=True)
+    secret = fev.UnicodeString()
+
+
+class WebhookEditForm(WebhookCreateForm):
+    def __init__(self, sender, app):
+        super(WebhookEditForm, self).__init__()
+        self.add_field('webhook', WebhookValidator(
+            sender=sender, app=app, not_empty=True))
+
+
+class WebhookControllerMeta(type):
+    def __call__(cls, sender, app, *args, **kw):
+        """Decorate post handlers with a validator that references
+        the appropriate webhook sender for this controller.
+        """
+        if hasattr(cls, 'create'):
+            cls.create = validate(
+                cls.create_form(),
+                error_handler=cls.index.__func__,
+            )(cls.create)
+        if hasattr(cls, 'edit'):
+            cls.edit = validate(
+                cls.edit_form(sender, app),
+                error_handler=cls._default.__func__,
+            )(cls.edit)
+        return type.__call__(cls, sender, app, *args, **kw)
+
+
+class WebhookController(BaseController, metaclass=WebhookControllerMeta):
+    create_form = WebhookCreateForm
+    edit_form = WebhookEditForm
+
+    def __init__(self, sender, app):
+        super(WebhookController, self).__init__()
+        self.sender = sender()
+        self.app = app
+
+    def gen_secret(self):
+        return h.cryptographic_nonce(20)
+
+    def update_webhook(self, wh, url, secret=None):
+        if not secret:
+            secret = self.gen_secret()
+        wh.hook_url = url
+        wh.secret = secret
+        try:
+            session(wh).flush(wh)
+        except DuplicateKeyError:
+            session(wh).expunge(wh)
+            msg = '_the_form: "{}" webhook already exists for {} {}'.format(
+                wh.type, self.app.config.options.mount_label, url)
+            raise Invalid(msg, None, None)
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/webhooks/create_form.html')
+    def index(self, **kw):
+        if not c.form_values and kw:
+            # Executes if update_webhook raises an error
+            c.form_values = {'url': kw.get('url'),
+                             'secret': kw.get('secret')}
+        return {'sender': self.sender,
+                'action': 'create',
+                'form': self.create_form()}
+
+    @expose()
+    @require_post()
+    def create(self, url, secret):
+        if self.sender.enforce_limit(self.app):
+            webhook = M.Webhook(
+                type=self.sender.type,
+                app_config_id=self.app.config._id)
+            self.update_webhook(webhook, url, secret)
+            M.AuditLog.log('add webhook %s %s %s',
+                           webhook.type, webhook.hook_url,
+                           webhook.app_config.url())
+            flash('Created successfully', 'ok')
+        else:
+            flash('You have exceeded the maximum number of webhooks '
+                  'you are allowed to create for this project/app', 'error')
+        redirect(self.app.admin_url + 'webhooks')
+
+    @expose()
+    @require_post()
+    def edit(self, webhook, url, secret):
+        old_url = webhook.hook_url
+        old_secret = webhook.secret
+        self.update_webhook(webhook, url, secret)
+        M.AuditLog.log('edit webhook %s\n%s => %s\n%s',
+                       webhook.type, old_url, url,
+                       'secret changed' if old_secret != secret else '')
+        flash('Edited successfully', 'ok')
+        redirect(self.app.admin_url + 'webhooks')
+
+    @expose('json:')
+    @require_post()
+    def delete(self, webhook):
+        form = self.edit_form(self.sender, self.app)
+        try:
+            wh = form.fields['webhook'].to_python(webhook)
+        except Invalid:
+            raise exc.HTTPNotFound()
+        wh.delete()
+        M.AuditLog.log('delete webhook %s %s %s',
+                       wh.type, wh.hook_url, wh.app_config.url())
+        return {'status': 'ok'}
+
+    @without_trailing_slash
+    @expose('jinja:allura:templates/webhooks/create_form.html')
+    def _default(self, webhook, **kw):
+        form = self.edit_form(self.sender, self.app)
+        try:
+            wh = form.fields['webhook'].to_python(webhook)
+        except Invalid:
+            raise exc.HTTPNotFound()
+        c.form_values = {'url': kw.get('url') or wh.hook_url,
+                         'secret': kw.get('secret') or wh.secret,
+                         'webhook': str(wh._id)}
+        return {'sender': self.sender,
+                'action': 'edit',
+                'form': form}
+
+
+class WebhookRestController(BaseController):
+    def __init__(self, sender, app):
+        super(WebhookRestController, self).__init__()
+        self.sender = sender()
+        self.app = app
+        self.create_form = WebhookController.create_form
+        self.edit_form = WebhookController.edit_form
+
+    def _error(self, e):
+        error = getattr(e, 'error_dict', None)
+        if error:
+            _error = {}
+            for k, v in list(error.items()):
+                _error[k] = str(v)
+            return _error
+        error = getattr(e, 'msg', None)
+        if not error:
+            error = getattr(e, 'message', '')
+        return error
+
+    def update_webhook(self, wh, url, secret=None):
+        controller = WebhookController(self.sender.__class__, self.app)
+        controller.update_webhook(wh, url, secret)
+
+    @expose('json:')
+    @require_post()
+    def index(self, **kw):
+        response.content_type = 'application/json'
+        try:
+            params = {'secret': kw.pop('secret', ''),
+                      'url': kw.pop('url', None)}
+            valid = self.create_form().to_python(params)
+        except Exception as e:
+            response.status_int = 400
+            return {'result': 'error', 'error': self._error(e)}
+        if self.sender.enforce_limit(self.app):
+            webhook = M.Webhook(
+                type=self.sender.type,
+                app_config_id=self.app.config._id)
+            try:
+                self.update_webhook(webhook, valid['url'], valid['secret'])
+            except Invalid as e:
+                response.status_int = 400
+                return {'result': 'error', 'error': self._error(e)}
+            M.AuditLog.log('add webhook %s %s %s',
+                           webhook.type, webhook.hook_url,
+                           webhook.app_config.url())
+            response.status_int = 201
+            # refetch updated values (e.g. mod_date)
+            session(webhook).expunge(webhook)
+            webhook = M.Webhook.query.get(_id=webhook._id)
+            return webhook.__json__()
+        else:
+            limits = {
+                'max': M.Webhook.max_hooks(
+                    self.sender.type,
+                    self.app.config.tool_name),
+                'used': M.Webhook.query.find({
+                    'type': self.sender.type,
+                    'app_config_id': self.app.config._id,
+                }).count(),
+            }
+            resp = {
+                'result': 'error',
+                'error': 'You have exceeded the maximum number of webhooks '
+                         'you are allowed to create for this project/app',
+                'limits': limits,
+            }
+            response.status_int = 400
+            return resp
+
+    @expose('json:')
+    def _default(self, webhook, **kw):
+        form = self.edit_form(self.sender, self.app)
+        try:
+            wh = form.fields['webhook'].to_python(webhook)
+        except Invalid:
+            raise exc.HTTPNotFound()
+        if request.method == 'POST':
+            return self._edit(wh, form, **kw)
+        elif request.method == 'DELETE':
+            return self._delete(wh)
+        else:
+            return wh.__json__()
+
+    def _edit(self, webhook, form, **kw):
+        old_secret = webhook.secret
+        old_url = webhook.hook_url
+        try:
+            params = {'secret': kw.pop('secret', old_secret),
+                      'url': kw.pop('url', old_url),
+                      'webhook': str(webhook._id)}
+            valid = form.to_python(params)
+        except Exception as e:
+            response.status_int = 400
+            return {'result': 'error', 'error': self._error(e)}
+        try:
+            self.update_webhook(webhook, valid['url'], valid['secret'])
+        except Invalid as e:
+            response.status_int = 400
+            return {'result': 'error', 'error': self._error(e)}
+        M.AuditLog.log(
+            'edit webhook %s\n%s => %s\n%s',
+            webhook.type, old_url, valid['url'],
+            'secret changed' if old_secret != valid['secret'] else '')
+        # refetch updated values (e.g. mod_date)
+        session(webhook).expunge(webhook)
+        webhook = M.Webhook.query.get(_id=webhook._id)
+        return webhook.__json__()
+
+    def _delete(self, webhook):
+        webhook.delete()
+        M.AuditLog.log(
+            'delete webhook %s %s %s',
+            webhook.type,
+            webhook.hook_url,
+            webhook.app_config.url())
+        return {'result': 'ok'}
+
+
+class SendWebhookHelper(object):
+    def __init__(self, webhook, payload):
+        self.webhook = webhook
+        self.payload = payload
+
+    @property
+    def timeout(self):
+        return asint(config.get('webhook.timeout', 30))
+
+    @property
+    def retries(self):
+        t = aslist(config.get('webhook.retry', [60, 120, 240]))
+        return list(map(int, t))
+
+    def sign(self, json_payload):
+        signature = hmac.new(
+            self.webhook.secret.encode('utf-8'),
+            json_payload.encode('utf-8'),
+            hashlib.sha1)
+        return 'sha1=' + signature.hexdigest()
+
+    def log_msg(self, msg, response=None):
+        message = '{}: {} {} {}'.format(
+            msg,
+            self.webhook.type,
+            self.webhook.hook_url,
+            self.webhook.app_config.url())
+        if response is not None:
+            message = '{} {} {} {}'.format(
+                message,
+                response.status_code,
+                response.text,
+                response.headers)
+        return message
+
+    def send(self):
+        json_payload = json.dumps(self.payload, cls=DateJSONEncoder)
+        signature = self.sign(json_payload)
+        headers = {'content-type': 'application/json',
+                   'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
+                   'X-Allura-Signature': signature}
+        ok = self._send(self.webhook.hook_url, json_payload, headers)
+        if not ok:
+            log.info('Retrying webhook in: %s', self.retries)
+            for t in self.retries:
+                log.info('Retrying webhook in %s seconds', t)
+                time.sleep(t)
+                ok = self._send(self.webhook.hook_url, json_payload, headers)
+                if ok:
+                    return
+
+    def _send(self, url, data, headers):
+        try:
+            r = requests.post(
+                url,
+                data=data,
+                headers=headers,
+                timeout=self.timeout)
+        except (requests.exceptions.RequestException,
+                socket.timeout,
+                ssl.SSLError):
+            log.exception(self.log_msg('Webhook send error'))
+            return False
+        if r.status_code >= 200 and r.status_code < 300:
+            log.info(self.log_msg('Webhook successfully sent'))
+            return True
+        else:
+            log.error(self.log_msg('Webhook send error', response=r))
+            return False
+
+
+@task()
+def send_webhook(webhook_id, payload):
+    webhook = M.Webhook.query.get(_id=webhook_id)
+    SendWebhookHelper(webhook, payload).send()
+
+
+class WebhookSender(object):
+    """Base class for webhook senders.
+
+    Subclasses are required to implement :meth:`get_payload()` and set
+    :attr:`type` and :attr:`triggered_by`.
+    """
+
+    type = None
+    triggered_by = []
+    controller = WebhookController
+    api_controller = WebhookRestController
+
+    def get_payload(self, **kw):
+        """Return a dict with webhook payload"""
+        raise NotImplementedError('get_payload')
+
+    def send(self, params_or_list):
+        """Post a task that will send webhook payload
+
+        :param params_or_list: dict with keyword parameters to be passed to
+            :meth:`get_payload` or a list of such dicts. If it's a list for each
+            element appropriate payload will be submitted, but limit will be
+            enforced only once for each webhook.
+        """
+        if not isinstance(params_or_list, list):
+            params_or_list = [params_or_list]
+        webhooks = M.Webhook.query.find(dict(
+            app_config_id=c.app.config._id,
+            type=self.type,
+        )).all()
+        if webhooks:
+            payloads = [self.get_payload(**params)
+                        for params in params_or_list]
+            for webhook in webhooks:
+                if webhook.enforce_limit():
+                    webhook.update_limit()
+                    for payload in payloads:
+                        send_webhook.post(webhook._id, payload)
+                else:
+                    log.warn('Webhook fires too often: %s. Skipping', webhook)
+
+    def enforce_limit(self, app):
+        '''
+        Checks if limit of webhooks created for given project/app is reached.
+        Returns False if limit is reached, True otherwise.
+        '''
+        count = M.Webhook.query.find(dict(
+            app_config_id=app.config._id,
+            type=self.type,
+        )).count()
+        limit = M.Webhook.max_hooks(self.type, app.config.tool_name)
+        return count < limit
+
+
+class RepoPushWebhookSender(WebhookSender):
+    type = 'repo-push'
+    triggered_by = ['git', 'hg', 'svn']
+
+    def _before(self, repo, commit_ids):
+        if len(commit_ids) > 0:
+            ci = commit_ids[-1]
+            parents = repo.commit(ci).parent_ids
+            if len(parents) > 0:
+                # Merge commit will have multiple parents. As far as I can tell
+                # the last one will be the branch head before merge
+                return self._convert_id(parents[-1])
+        return ''
+
+    def _after(self, commit_ids):
+        if len(commit_ids) > 0:
+            return self._convert_id(commit_ids[0])
+        return ''
+
+    def _convert_id(self, _id):
+        if ':' in _id:
+            _id = 'r' + _id.rsplit(':', 1)[1]
+        return _id
+
+    def get_payload(self, commit_ids, **kw):
+        app = kw.get('app') or c.app
+        commits = [app.repo.commit(ci).webhook_info for ci in commit_ids]
+        for ci in commits:
+            ci['id'] = self._convert_id(ci['id'])
+        before = self._before(app.repo, commit_ids)
+        after = self._after(commit_ids)
+        payload = {
+            'size': len(commits),
+            'commits': commits,
+            'before': before,
+            'after': after,
+            'repository': {
+                'name': app.config.options.mount_label,
+                'full_name': app.url,
+                'url': h.absurl(app.url),
+            },
+        }
+        if kw.get('ref'):
+            payload['ref'] = kw['ref']
+        return payload

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/websetup/__init__.py
----------------------------------------------------------------------
diff --git a/websetup/__init__.py b/websetup/__init__.py
new file mode 100644
index 0000000..cc62fba
--- /dev/null
+++ b/websetup/__init__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+#       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.
+
+"""Setup the allura application"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import logging
+
+from allura.config.environment import load_environment
+
+__all__ = ['setup_app']
+
+log = logging.getLogger(__name__)
+
+from .schema import setup_schema
+from . import bootstrap
+
+
+def setup_app(command, conf, vars):
+    """Place any commands to setup allura here"""
+    load_environment(conf.global_conf, conf.local_conf)
+    setup_schema(command, conf, vars)
+    bootstrap.bootstrap(command, conf, vars)

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/websetup/bootstrap.py
----------------------------------------------------------------------
diff --git a/websetup/bootstrap.py b/websetup/bootstrap.py
new file mode 100644
index 0000000..4a209ef
--- /dev/null
+++ b/websetup/bootstrap.py
@@ -0,0 +1,357 @@
+# -*- coding: utf-8 -*-
+
+#       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.
+
+"""Setup the allura application"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+import os
+import sys
+import logging
+import shutil
+from collections import defaultdict
+from textwrap import dedent
+
+import tg
+from pylons import tmpl_context as c, app_globals as g
+from paste.deploy.converters import asbool
+import ew
+
+from ming import Session, mim
+from ming.orm import state, session
+from ming.orm.ormsession import ThreadLocalORMSession
+
+import allura
+from allura.lib import plugin
+from allura.lib import helpers as h
+from allura import model as M
+from allura.command import EnsureIndexCommand
+from allura.command import CreateTroveCategoriesCommand
+from allura.websetup.schema import REGISTRY
+
+from forgewiki import model as WM
+
+log = logging.getLogger(__name__)
+
+
+def cache_test_data():
+    log.info('Saving data to cache in .test-data')
+    if os.path.exists('.test-data'):
+        shutil.rmtree('.test-data')
+    os.system(
+        'mongodump -h 127.0.0.1:27018 -o .test-data > mongodump.log 2>&1')
+
+
+def restore_test_data():
+    if os.path.exists('.test-data'):
+        log.info('Restoring data from cache in .test-data')
+        rc = os.system(
+            'mongorestore -h 127.0.0.1:27018 --dir .test-data > mongorestore.log 2>&1')
+        return rc == 0
+    else:
+        return False
+
+
+def bootstrap(command, conf, vars):
+    """Place any commands to setup allura here"""
+    # are we being called by the test suite?
+    test_run = conf.get('__file__', '').endswith('test.ini')
+
+    if not test_run:
+        # when this is run via the `setup-app` cmd, some macro rendering needs this set up
+        REGISTRY.register(ew.widget_context,
+                          ew.core.WidgetContext('http', ew.ResourceManager()))
+
+    # if this is a test_run, skip user project creation to save time
+    make_user_projects = not test_run
+
+    def make_user(*args, **kw):
+        kw.update(make_project=make_user_projects)
+        return create_user(*args, **kw)
+
+    # Temporarily disable auth extensions to prevent unintended side-effects
+    tg.config['auth.method'] = tg.config['registration.method'] = 'local'
+    assert tg.config['auth.method'] == 'local'
+    conf['auth.method'] = conf['registration.method'] = 'local'
+
+    # Clean up all old stuff
+    ThreadLocalORMSession.close_all()
+    c.user = c.project = c.app = None
+    wipe_database()
+    try:
+        g.solr.delete(q='*:*')
+    except:  # pragma no cover
+        log.error('SOLR server is %s', g.solr_server)
+        log.error('Error clearing solr index')
+
+    # set up mongo indexes
+    index = EnsureIndexCommand('ensure_index')
+    index.run([''])
+
+    if asbool(conf.get('cache_test_data')):
+        if restore_test_data():
+            h.set_context('test', neighborhood='Projects')
+            return
+    log.info('Initializing search')
+
+    log.info('Registering root user & default neighborhoods')
+    anonymous = M.User(
+        _id=None,
+        username='*anonymous',
+        display_name='Anonymous')
+
+    # never make a user project for the root user
+    root = create_user('Root', make_project=False)
+
+    n_projects = M.Neighborhood(name='Projects', url_prefix='/p/',
+                                features=dict(private_projects=True,
+                                              max_projects=None,
+                                              css='none',
+                                              google_analytics=False))
+    n_users = M.Neighborhood(name='Users', url_prefix='/u/',
+                             shortname_prefix='u/',
+                             anchored_tools='profile:Profile,userstats:Statistics',
+                             features=dict(private_projects=True,
+                                           max_projects=None,
+                                           css='none',
+                                           google_analytics=False))
+    n_adobe = M.Neighborhood(
+        name='Adobe', url_prefix='/adobe/', project_list_url='/adobe/',
+        features=dict(private_projects=True,
+                      max_projects=None,
+                      css='custom',
+                      google_analytics=True))
+    assert tg.config['auth.method'] == 'local'
+    project_reg = plugin.ProjectRegistrationProvider.get()
+    p_projects = project_reg.register_neighborhood_project(
+        n_projects, [root], allow_register=True)
+    p_users = project_reg.register_neighborhood_project(n_users, [root])
+    p_adobe = project_reg.register_neighborhood_project(n_adobe, [root])
+
+    def set_nbhd_wiki_content(nbhd_proj, content):
+        wiki = nbhd_proj.app_instance('wiki')
+        page = WM.Page.query.get(
+            app_config_id=wiki.config._id, title=wiki.root_page_name)
+        page.text = content
+
+    set_nbhd_wiki_content(p_projects, dedent('''
+        Welcome to the "Projects" neighborhood.  It is the default neighborhood in Allura.
+        You can edit this wiki page as you see fit.  Here's a few ways to get started:
+
+        [Register a new project](/p/add_project)
+
+        [Neighborhood administration](/p/admin)
+
+        [[projects show_total=yes]]
+        '''))
+    set_nbhd_wiki_content(p_users, dedent('''
+        This is the "Users" neighborhood.  All users automatically get a user-project created for them, using their username.
+
+        [Neighborhood administration](/u/admin)
+
+        [[projects show_total=yes]]
+        '''))
+    set_nbhd_wiki_content(p_adobe, dedent('''
+        This is the "Adobe" neighborhood.  It is just an example of having projects in a different neighborhood.
+
+        [Neighborhood administration](/adobe/admin)
+
+        [[projects show_total=yes]]
+        '''))
+
+    ThreadLocalORMSession.flush_all()
+    ThreadLocalORMSession.close_all()
+
+    # add the adobe icon
+    file_name = 'adobe_icon.png'
+    file_path = os.path.join(
+        allura.__path__[0], 'public', 'nf', 'images', file_name)
+    M.NeighborhoodFile.from_path(file_path, neighborhood_id=n_adobe._id)
+
+    # Add some test users
+    for unum in range(10):
+        make_user('Test User %d' % unum)
+
+    log.info('Creating basic project categories')
+    cat1 = M.ProjectCategory(name='clustering', label='Clustering')
+
+    cat2 = M.ProjectCategory(name='communications', label='Communications')
+    cat2_1 = M.ProjectCategory(
+        name='synchronization', label='Synchronization', parent_id=cat2._id)
+    cat2_2 = M.ProjectCategory(
+        name='streaming', label='Streaming', parent_id=cat2._id)
+    cat2_3 = M.ProjectCategory(name='fax', label='Fax', parent_id=cat2._id)
+    cat2_4 = M.ProjectCategory(name='bbs', label='BBS', parent_id=cat2._id)
+
+    cat3 = M.ProjectCategory(name='database', label='Database')
+    cat3_1 = M.ProjectCategory(
+        name='front_ends', label='Front-Ends', parent_id=cat3._id)
+    cat3_2 = M.ProjectCategory(
+        name='engines_servers', label='Engines/Servers', parent_id=cat3._id)
+
+    log.info('Registering "regular users" (non-root) and default projects')
+    # since this runs a lot for tests, separate test and default users and
+    # do the minimal needed
+    if asbool(conf.get('load_test_data')):
+        u_admin = make_user('Test Admin')
+        u_admin.preferences = dict(email_address='test-admin@users.localhost')
+        u_admin.email_addresses = ['test-admin@users.localhost']
+        u_admin.set_password('foo')
+        u_admin.claim_address('test-admin@users.localhost')
+        ThreadLocalORMSession.flush_all()
+
+        admin_email = M.EmailAddress.get(email='test-admin@users.localhost')
+        admin_email.confirmed = True
+    else:
+        u_admin = make_user('Admin 1', username='admin1')
+        # Admin1 is almost root, with admin access for Users and Projects
+        # neighborhoods
+        p_projects.add_user(u_admin, ['Admin'])
+        p_users.add_user(u_admin, ['Admin'])
+
+        p_allura = n_projects.register_project('allura', u_admin, 'Allura')
+    u1 = make_user('Test User')
+    p_adobe1 = n_adobe.register_project('adobe-1', u_admin, 'Adobe project 1')
+    p_adobe.add_user(u_admin, ['Admin'])
+    p0 = n_projects.register_project('test', u_admin, 'Test Project')
+    p1 = n_projects.register_project('test2', u_admin, 'Test 2')
+    p0._extra_tool_status = ['alpha', 'beta']
+
+    sess = session(M.Neighborhood)  # all the sessions are the same
+    for x in (n_adobe, n_projects, n_users, p_projects, p_users, p_adobe):
+        # Ming doesn't detect substructural changes in newly created objects
+        # (vs loaded from DB)
+        state(x).status = 'dirty'
+        # TODO: Hope that Ming can be improved to at least avoid stuff below
+        sess.flush(x)
+
+    ThreadLocalORMSession.flush_all()
+
+    if asbool(conf.get('load_test_data')):
+        if asbool(conf.get('cache_test_data')):
+            cache_test_data()
+    else:  # pragma no cover
+        # regular first-time setup
+
+        create_trove_categories = CreateTroveCategoriesCommand('create_trove_categories')
+        create_trove_categories.run([''])
+
+        p0.add_user(u_admin, ['Admin'])
+        log.info('Registering initial apps')
+        with h.push_config(c, user=u_admin):
+            p0.install_apps([{'ep_name': ep_name}
+                for ep_name, app in g.entry_points['tool'].items()
+                if app._installable(tool_name=ep_name,
+                                    nbhd=n_projects,
+                                    project_tools=[])
+            ])
+
+    # reload our p0 project so that p0.app_configs is accurate with all the
+    # newly installed apps
+    ThreadLocalORMSession.flush_all()
+    ThreadLocalORMSession.close_all()
+    p0 = M.Project.query.get(_id=p0._id)
+    sub = p0.new_subproject('sub1', project_name='A Subproject')
+    with h.push_config(c, user=u_admin):
+        sub.install_app('wiki')
+
+    ThreadLocalORMSession.flush_all()
+    ThreadLocalORMSession.close_all()
+
+
+def wipe_database():
+    conn = M.main_doc_session.bind.conn
+    if isinstance(conn, mim.Connection):
+        clear_all_database_tables()
+        for db in conn.database_names():
+            db = conn[db]
+    else:
+        for database in conn.database_names():
+            if database not in ('allura', 'pyforge', 'project-data'):
+                continue
+            log.info('Wiping database %s', database)
+            db = conn[database]
+            for coll in db.collection_names():
+                if coll.startswith('system.'):
+                    continue
+                log.info('Dropping collection %s:%s', database, coll)
+                try:
+                    db.drop_collection(coll)
+                except:
+                    pass
+
+
+def clear_all_database_tables():
+    conn = M.main_doc_session.bind.conn
+    for db in conn.database_names():
+        db = conn[db]
+        for coll in db.collection_names():
+            if coll == 'system.indexes':
+                continue
+            db.drop_collection(coll)
+
+
+def create_user(display_name, username=None, password='foo', make_project=False):
+    if not username:
+        username = display_name.lower().replace(' ', '-')
+    user = M.User.register(dict(username=username,
+                                display_name=display_name),
+                           make_project=make_project)
+    user.set_password(password)
+    return user
+
+
+class DBSession(Session):
+
+    '''Simple session that takes a pymongo connection and a database name'''
+
+    def __init__(self, db):
+        self._db = db
+
+    @property
+    def db(self):
+        return self._db
+
+    def _impl(self, cls):
+        return self.db[cls.__mongometa__.name]
+
+
+def pm(etype, value, tb):  # pragma no cover
+    import pdb
+    import traceback
+    try:
+        from IPython.ipapi import make_session
+        make_session()
+        from IPython.Debugger import Pdb
+        sys.stderr.write('Entering post-mortem IPDB shell\n')
+        p = Pdb(color_scheme='Linux')
+        p.reset()
+        p.setup(None, tb)
+        p.print_stack_trace()
+        sys.stderr.write('%s: %s\n' % (etype, value))
+        p.cmdloop()
+        p.forget()
+        # p.interaction(None, tb)
+    except ImportError:
+        sys.stderr.write('Entering post-mortem PDB shell\n')
+        traceback.print_exception(etype, value, tb)
+        pdb.post_mortem(tb)
+
+sys.excepthook = pm

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/websetup/schema.py
----------------------------------------------------------------------
diff --git a/websetup/schema.py b/websetup/schema.py
new file mode 100644
index 0000000..acf65d0
--- /dev/null
+++ b/websetup/schema.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+#       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.
+
+"""Setup the allura application"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import logging
+
+import activitystream
+from tg import config
+import pylons
+from paste.deploy.converters import asbool
+from paste.registry import Registry
+
+log = logging.getLogger(__name__)
+REGISTRY = Registry()
+
+
+def setup_schema(command, conf, vars):
+    """Place any commands to setup allura here"""
+    import ming
+    import allura
+
+    REGISTRY.prepare()
+    REGISTRY.register(pylons.tmpl_context, EmptyClass())
+    REGISTRY.register(pylons.app_globals, config['pylons.app_globals'])
+    REGISTRY.register(allura.credentials, allura.lib.security.Credentials())
+    ming.configure(**conf)
+    if asbool(conf.get('activitystream.recording.enabled', False)):
+        activitystream.configure(**conf)
+    # Nothing to do
+    log.info('setup_schema called')
+
+
+class EmptyClass(object):
+    pass