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:37 UTC

[15/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/tasks/mail_tasks.py
----------------------------------------------------------------------
diff --git a/tasks/mail_tasks.py b/tasks/mail_tasks.py
new file mode 100644
index 0000000..a771f03
--- /dev/null
+++ b/tasks/mail_tasks.py
@@ -0,0 +1,209 @@
+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 html.parser
+
+from pylons import tmpl_context as c, app_globals as g
+from bson import ObjectId
+
+from allura.lib import helpers as h
+from allura.lib.decorators import task
+from allura.lib import mail_util
+from allura.lib import exceptions as exc
+
+log = logging.getLogger(__name__)
+
+smtp_client = mail_util.SMTPClient()
+
+
+@task
+def route_email(
+        peer, mailfrom, rcpttos, data):
+    '''Route messages according to their destination:
+
+    <topic>@<mount_point>.<subproj2>.<subproj1>.<project>.projects.domain.net
+    gets sent to c.app.handle_message(topic, message)
+    '''
+    try:
+        msg = mail_util.parse_message(data)
+    except:  # pragma no cover
+        log.exception('Parse Error: (%r,%r,%r)', peer, mailfrom, rcpttos)
+        return
+    if mail_util.is_autoreply(msg):
+        log.info('Skipping autoreply message: %s', msg['headers'])
+        return
+    mail_user = mail_util.identify_sender(peer, mailfrom, msg['headers'], msg)
+    with h.push_config(c, user=mail_user):
+        log.info('Received email from %s', c.user.username)
+        # For each of the addrs, determine the project/app and route
+        # appropriately
+        for addr in rcpttos:
+            try:
+                userpart, project, app = mail_util.parse_address(addr)
+                with h.push_config(c, project=project, app=app):
+                    if not app.has_access(c.user, userpart):
+                        log.info('Access denied for %s to mailbox %s',
+                                 c.user, userpart)
+                    elif not c.app.config.options.get('AllowEmailPosting', True):
+                        log.info("Posting from email is not enabled")
+                    else:
+                        if msg['multipart']:
+                            msg_hdrs = msg['headers']
+                            for part in msg['parts']:
+                                if part.get('content_type', '').startswith('multipart/'):
+                                    continue
+                                msg = dict(
+                                    headers=dict(msg_hdrs, **part['headers']),
+                                    message_id=part['message_id'],
+                                    in_reply_to=part['in_reply_to'],
+                                    references=part['references'],
+                                    filename=part['filename'],
+                                    content_type=part['content_type'],
+                                    payload=part['payload'])
+                                c.app.handle_message(userpart, msg)
+                        else:
+                            c.app.handle_message(userpart, msg)
+            except exc.MailError as e:
+                log.error('Error routing email to %s: %s', addr, e)
+            except:
+                log.exception('Error routing mail to %s', addr)
+
+
+@task
+def sendmail(fromaddr, destinations, text, reply_to, subject,
+             message_id, in_reply_to=None, sender=None, references=None):
+    '''
+    Send an email to the specified list of destinations with respect to the preferred email format specified by user.
+    It is best for broadcast messages.
+
+    :param fromaddr: ObjectId or str(ObjectId) of user, or email address str
+
+    '''
+    from allura import model as M
+    addrs_plain = []
+    addrs_html = []
+    addrs_multi = []
+    if fromaddr is None:
+        fromaddr = g.noreply
+    elif not isinstance(fromaddr, str) or '@' not in fromaddr:
+        log.warning('Looking up user with fromaddr: %s', fromaddr)
+        user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
+        if not user:
+            log.warning('Cannot find user with ID: %s', fromaddr)
+            fromaddr = g.noreply
+        else:
+            fromaddr = user.email_address_header()
+    # Divide addresses based on preferred email formats
+    for addr in destinations:
+        if mail_util.isvalid(addr):
+            addrs_plain.append(addr)
+        else:
+            try:
+                user = M.User.query.get(_id=ObjectId(addr), disabled=False, pending=False)
+                if not user:
+                    log.warning('Cannot find user with ID: %s', addr)
+                    continue
+            except:
+                log.exception('Error looking up user with ID: %r' % addr)
+                continue
+            addr = user.email_address_header()
+            if not addr and user.email_addresses:
+                addr = user.email_addresses[0]
+                log.warning(
+                    'User %s has not set primary email address, using %s',
+                    user._id, addr)
+            if not addr:
+                log.error(
+                    "User %s (%s) has not set any email address, can't deliver",
+                    user._id, user.username)
+                continue
+            if user.get_pref('email_format') == 'plain':
+                addrs_plain.append(addr)
+            elif user.get_pref('email_format') == 'html':
+                addrs_html.append(addr)
+            else:
+                addrs_multi.append(addr)
+    htmlparser = html.parser.HTMLParser()
+    plain_msg = mail_util.encode_email_part(htmlparser.unescape(text), 'plain')
+    html_text = g.forge_markdown(email=True).convert(text)
+    html_msg = mail_util.encode_email_part(html_text, 'html')
+    multi_msg = mail_util.make_multipart_message(plain_msg, html_msg)
+    smtp_client.sendmail(
+        addrs_multi, fromaddr, reply_to, subject, message_id,
+        in_reply_to, multi_msg, sender=sender, references=references)
+    smtp_client.sendmail(
+        addrs_plain, fromaddr, reply_to, subject, message_id,
+        in_reply_to, plain_msg, sender=sender, references=references)
+    smtp_client.sendmail(
+        addrs_html, fromaddr, reply_to, subject, message_id,
+        in_reply_to, html_msg, sender=sender, references=references)
+
+
+@task
+def sendsimplemail(
+        fromaddr,
+        toaddr,
+        text,
+        reply_to,
+        subject,
+        message_id,
+        in_reply_to=None,
+        sender=None,
+        references=None,
+        cc=None):
+    '''
+    Send a single mail to the specified address.
+    It is best for single user notifications.
+
+    :param fromaddr: ObjectId or str(ObjectId) of user, or email address str
+    :param toaddr: ObjectId or str(ObjectId) of user, or email address str
+
+    '''
+    from allura import model as M
+    if fromaddr is None:
+        fromaddr = g.noreply
+    elif not isinstance(fromaddr, str) or '@' not in fromaddr:
+        log.warning('Looking up user with fromaddr: %s', fromaddr)
+        user = M.User.query.get(_id=ObjectId(fromaddr), disabled=False, pending=False)
+        if not user:
+            log.warning('Cannot find user with ID: %s', fromaddr)
+            fromaddr = g.noreply
+        else:
+            fromaddr = user.email_address_header()
+
+    if not isinstance(toaddr, str) or '@' not in toaddr:
+        log.warning('Looking up user with toaddr: %s', toaddr)
+        user = M.User.query.get(_id=ObjectId(toaddr), disabled=False, pending=False)
+        if not user:
+            log.warning('Cannot find user with ID: %s', toaddr)
+            toaddr = g.noreply
+        else:
+            toaddr = user.email_address_header()
+
+    htmlparser = html.parser.HTMLParser()
+    plain_msg = mail_util.encode_email_part(htmlparser.unescape(text), 'plain')
+    html_text = g.forge_markdown(email=True).convert(text)
+    html_msg = mail_util.encode_email_part(html_text, 'html')
+    multi_msg = mail_util.make_multipart_message(plain_msg, html_msg)
+    smtp_client.sendmail(
+        [toaddr], fromaddr, reply_to, subject, message_id,
+        in_reply_to, multi_msg, sender=sender, references=references, cc=cc, to=toaddr)

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tasks/notification_tasks.py
----------------------------------------------------------------------
diff --git a/tasks/notification_tasks.py b/tasks/notification_tasks.py
new file mode 100644
index 0000000..2244b46
--- /dev/null
+++ b/tasks/notification_tasks.py
@@ -0,0 +1,29 @@
+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.
+
+from allura.lib.decorators import task
+
+
+@task
+def notify(n_id, ref_id, topic):
+    from allura import model as M
+    M.Mailbox.deliver(n_id, ref_id, topic)
+    M.Mailbox.fire_ready()

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tasks/repo_tasks.py
----------------------------------------------------------------------
diff --git a/tasks/repo_tasks.py b/tasks/repo_tasks.py
new file mode 100644
index 0000000..d747867
--- /dev/null
+++ b/tasks/repo_tasks.py
@@ -0,0 +1,176 @@
+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 shutil
+import logging
+import traceback
+
+from pylons import tmpl_context as c, app_globals as g
+from ming.odm import session
+
+from allura.lib.decorators import task
+from allura.lib.repository import RepositoryApp
+
+
+@task
+def init(**kwargs):
+    from allura import model as M
+    c.app.repo.init()
+    M.Notification.post_user(
+        c.user, c.app.repo, 'created',
+        text='Repository %s/%s created' % (
+            c.project.shortname, c.app.config.options.mount_point))
+
+
+@task
+def clone(cloned_from_path, cloned_from_name, cloned_from_url):
+    from allura import model as M
+    try:
+        c.app.repo.init_as_clone(
+            cloned_from_path,
+            cloned_from_name,
+            cloned_from_url)
+        M.Notification.post_user(
+            c.user, c.app.repo, 'created',
+            text='Repository %s/%s created' % (
+                c.project.shortname, c.app.config.options.mount_point))
+    except Exception:
+        g.post_event('repo_clone_task_failed', cloned_from_url,
+                     cloned_from_path, traceback.format_exc())
+
+
+@task
+def reclone(*args, **kwargs):
+    from allura import model as M
+    from ming.orm import ThreadLocalORMSession
+    repo = c.app.repo
+    if repo is not None:
+        shutil.rmtree(repo.full_fs_path, ignore_errors=True)
+    M.MergeRequest.query.remove(dict(
+        app_config_id=c.app.config._id))
+    ThreadLocalORMSession.flush_all()
+    clone(*args, **kwargs)
+
+
+@task
+def refresh(**kwargs):
+    from allura import model as M
+    log = logging.getLogger(__name__)
+    # don't create multiple refresh tasks
+    q = {
+        'task_name': 'allura.tasks.repo_tasks.refresh',
+        'state': {'$in': ['busy', 'ready']},
+        'context.app_config_id': c.app.config._id,
+        'context.project_id': c.project._id,
+    }
+    refresh_tasks_count = M.MonQTask.query.find(q).count()
+    if refresh_tasks_count <= 1:  # only this task
+        c.app.repo.refresh()
+        # checking if we have new commits arrived
+        # during refresh and re-queue task if so
+        new_commit_ids = c.app.repo.unknown_commit_ids()
+        if len(new_commit_ids) > 0:
+            refresh.post()
+            log.info('New refresh task is queued due to new commit(s).')
+    else:
+        log.info('Refresh task for %s:%s skipped due to backlog',
+                 c.project.shortname, c.app.config.options.mount_point)
+
+
+@task
+def uninstall(**kwargs):
+    from allura import model as M
+    repo = c.app.repo
+    if repo is not None:
+        shutil.rmtree(repo.full_fs_path, ignore_errors=True)
+        repo.delete()
+    M.MergeRequest.query.remove(dict(
+        app_config_id=c.app.config._id))
+    super(RepositoryApp, c.app).uninstall(c.project)
+    from ming.orm import ThreadLocalORMSession
+    ThreadLocalORMSession.flush_all()
+
+
+@task
+def nop():
+    log = logging.getLogger(__name__)
+    log.info('nop')
+
+
+@task
+def reclone_repo(*args, **kwargs):
+    from allura import model as M
+    try:
+        nbhd = M.Neighborhood.query.get(url_prefix='/%s/' % kwargs['prefix'])
+        c.project = M.Project.query.get(
+            shortname=kwargs['shortname'], neighborhood_id=nbhd._id)
+        c.app = c.project.app_instance(kwargs['mount_point'])
+        source_url = c.app.config.options.get('init_from_url')
+        source_path = c.app.config.options.get('init_from_path')
+        c.app.repo.init_as_clone(source_path, None, source_url)
+        M.Notification.post_user(
+            c.user, c.app.repo, 'created',
+            text='Repository %s/%s created' % (
+                c.project.shortname, c.app.config.options.mount_point))
+    except Exception:
+        g.post_event('repo_clone_task_failed', source_url,
+                     source_path, traceback.format_exc())
+
+
+@task
+def tarball(revision, path):
+    log = logging.getLogger(__name__)
+    if revision:
+        repo = c.app.repo
+        status = repo.get_tarball_status(revision, path)
+        if status == 'complete':
+            log.info(
+                'Skipping snapshot for repository: %s:%s rev %s because it is already %s' %
+                (c.project.shortname, c.app.config.options.mount_point, revision, status))
+        else:
+            try:
+                repo.tarball(revision, path)
+            except:
+                log.error(
+                    'Could not create snapshot for repository: %s:%s revision %s path %s' %
+                    (c.project.shortname, c.app.config.options.mount_point, revision, path), exc_info=True)
+                raise
+    else:
+        log.warn(
+            'Skipped creation of snapshot: %s:%s because revision is not specified' %
+            (c.project.shortname, c.app.config.options.mount_point))
+
+
+@task
+def merge(merge_request_id):
+    from allura import model as M
+    mr = M.MergeRequest.query.get(_id=merge_request_id)
+    mr.app.repo.merge(mr)
+    mr.status = 'merged'
+    session(mr).flush(mr)
+
+
+@task
+def can_merge(merge_request_id):
+    from allura import model as M
+    mr = M.MergeRequest.query.get(_id=merge_request_id)
+    result = mr.app.repo.can_merge(mr)
+    mr.set_can_merge_cache(result)

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/__init__.py
----------------------------------------------------------------------
diff --git a/templates/__init__.py b/templates/__init__.py
new file mode 100644
index 0000000..4c0b4ac
--- /dev/null
+++ b/templates/__init__.py
@@ -0,0 +1,24 @@
+# -*- 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.
+
+"""Templates package for the application."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/discussion/__init__.py
----------------------------------------------------------------------
diff --git a/templates/discussion/__init__.py b/templates/discussion/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/templates/discussion/__init__.py
@@ -0,0 +1,16 @@
+#       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.

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/macro/__init__.py
----------------------------------------------------------------------
diff --git a/templates/macro/__init__.py b/templates/macro/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/templates/macro/__init__.py
@@ -0,0 +1,16 @@
+#       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.

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/oembed/__init__.py
----------------------------------------------------------------------
diff --git a/templates/oembed/__init__.py b/templates/oembed/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/templates/oembed/__init__.py
@@ -0,0 +1,16 @@
+#       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.

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/repo/__init__.py
----------------------------------------------------------------------
diff --git a/templates/repo/__init__.py b/templates/repo/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/templates/repo/__init__.py
@@ -0,0 +1,16 @@
+#       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.

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/templates/widgets/__init__.py
----------------------------------------------------------------------
diff --git a/templates/widgets/__init__.py b/templates/widgets/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/templates/widgets/__init__.py
@@ -0,0 +1,16 @@
+#       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.

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/__init__.py
----------------------------------------------------------------------
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..14e03bf
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,39 @@
+# -*- 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.
+
+"""Unit and functional test suite for allura."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import alluratest.controller
+
+# HACK: prevents test suite from crashing when running under the nose
+#       MultiProcessing plugin
+import socket
+socket.setdefaulttimeout(None)
+
+
+class TestController(alluratest.controller.TestController):
+
+    """
+    Base functional test case for the controllers.
+
+    """

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/decorators.py
----------------------------------------------------------------------
diff --git a/tests/decorators.py b/tests/decorators.py
new file mode 100644
index 0000000..635e03a
--- /dev/null
+++ b/tests/decorators.py
@@ -0,0 +1,201 @@
+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 sys
+import re
+from functools import wraps
+import contextlib
+
+from ming.orm.ormsession import ThreadLocalORMSession
+from pylons import tmpl_context as c
+from mock import patch
+import tg
+from paste.deploy.converters import asbool
+
+from allura import model as M
+import allura.config.middleware
+
+
+def with_user_project(username):
+    def _with_user_project(func):
+        @wraps(func)
+        def wrapped(*args, **kw):
+            user = M.User.by_username(username)
+            c.user = user
+            n = M.Neighborhood.query.get(name='Users')
+            shortname = 'u/' + username
+            p = M.Project.query.get(shortname=shortname, neighborhood_id=n._id)
+            if not p:
+                n.register_project(shortname, user=user, user_project=True)
+                ThreadLocalORMSession.flush_all()
+                ThreadLocalORMSession.close_all()
+            return func(*args, **kw)
+        return wrapped
+    return _with_user_project
+
+
+@contextlib.contextmanager
+def NullContextManager():
+    yield
+
+
+def with_tool(project_shortname, ep_name, mount_point=None, mount_label=None,
+              ordinal=None, post_install_hook=None, username='test-admin',
+              **override_options):
+    def _with_tool(func):
+        @wraps(func)
+        def wrapped(*args, **kw):
+            c.user = M.User.by_username(username)
+            p = M.Project.query.get(shortname=project_shortname)
+            c.project = p
+            if mount_point and not p.app_instance(mount_point):
+                c.app = p.install_app(
+                    ep_name, mount_point, mount_label, ordinal, **override_options)
+                if post_install_hook:
+                    post_install_hook(c.app)
+
+                if asbool(tg.config.get('smtp.mock')):
+                    smtp_mock = patch('allura.lib.mail_util.smtplib.SMTP')
+                else:
+                    smtp_mock = NullContextManager()
+                with smtp_mock:
+                    while M.MonQTask.run_ready('setup'):
+                        pass
+                ThreadLocalORMSession.flush_all()
+                ThreadLocalORMSession.close_all()
+            elif mount_point:
+                c.app = p.app_instance(mount_point)
+            return func(*args, **kw)
+        return wrapped
+    return _with_tool
+
+with_discussion = with_tool('test', 'Discussion', 'discussion')
+with_link = with_tool('test', 'Link', 'link')
+with_tracker = with_tool('test', 'Tickets', 'bugs')
+with_wiki = with_tool('test', 'Wiki', 'wiki')
+with_url = with_tool('test', 'ShortUrl', 'url')
+
+
+class raises(object):
+
+    '''
+    Test helper in the form of a context manager, to assert that something raises an exception.
+    After completion, the 'exc' attribute can be used to do further inspection of the exception
+    '''
+
+    def __init__(self, ExcType):
+        self.ExcType = ExcType
+        self.exc = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_t):
+        if exc_type:
+            self.exc = exc_val
+            if issubclass(exc_type, self.ExcType):
+                # ok
+                return True
+            else:
+                # root exception will be raised, untouched
+                return False
+        else:
+            raise AssertionError('Did not raise %s' % self.ExcType)
+
+
+def without_module(*module_names):
+    def _without_module(func):
+        @wraps(func)
+        def wrapped(*a, **kw):
+            with patch.dict(sys.modules, {m: None for m in module_names}):
+                return func(*a, **kw)
+        return wrapped
+    return _without_module
+
+
+class patch_middleware_config(object):
+
+    '''
+    Context manager that patches the configuration used during middleware
+    setup for Allura
+    '''
+
+    def __init__(self, new_configs):
+        self.new_configs = new_configs
+
+    def __enter__(self):
+        self._make_app = allura.config.middleware.make_app
+
+        def make_app(global_conf, full_stack=True, **app_conf):
+            app_conf.update(self.new_configs)
+            return self._make_app(global_conf, full_stack, **app_conf)
+
+        allura.config.middleware.make_app = make_app
+
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_t):
+        allura.config.middleware.make_app = self._make_app
+        return self
+
+
+@contextlib.contextmanager
+def audits(*messages, **kwargs):
+    """
+    Asserts all the messages exist in audit log
+
+    :param messages: regex strings
+    :param bool user: if this is a user log
+
+    """
+    M.AuditLog.query.remove()
+    yield
+    if kwargs.get('user'):
+        actor = kwargs.get('actor', '.*')
+        ip_addr = kwargs.get('ip_addr', '.*')
+        preamble = '(Done by user: {}\n)?IP Address: {}\n'.format(actor, ip_addr)
+    else:
+        preamble = ''
+    for message in messages:
+        assert M.AuditLog.query.find(dict(
+            message=re.compile(preamble + message))).count(), 'Could not find "%s"' % message
+
+
+@contextlib.contextmanager
+def out_audits(*messages, **kwargs):
+    """
+    Asserts none the messages exist in audit log.  "without audits"
+
+    :param messages: list of regex strings
+    :param bool user: if this is a user log
+
+    """
+    M.AuditLog.query.remove()
+    yield
+    if kwargs.get('user'):
+        actor = kwargs.get('actor', '.*')
+        ip_addr = kwargs.get('ip_addr', '.*')
+        preamble = '(Done by user: {}\n)?IP Address: {}\n'.format(actor, ip_addr)
+    else:
+        preamble = ''
+    for message in messages:
+        assert not M.AuditLog.query.find(dict(
+            message=re.compile(preamble + message))).count(), 'Found unexpected: "%s"' % message

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/__init__.py
----------------------------------------------------------------------
diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py
new file mode 100644
index 0000000..ba896c1
--- /dev/null
+++ b/tests/functional/__init__.py
@@ -0,0 +1,24 @@
+# -*- 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.
+
+"""Functional test suite for the controllers of the application."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals