You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2012/09/25 04:13:10 UTC

[12/14] git commit: [#4637] ticket:146 Create URL shortener Allura App

[#4637] ticket:146 Create URL shortener Allura App


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

Branch: refs/heads/master
Commit: 31f5f3ff06c922c57a98b5b8f9a82437c25ece82
Parents: 593dfcc
Author: Yuriy Arhipov <yu...@yandex.ru>
Authored: Mon Sep 3 18:15:02 2012 +0400
Committer: Cory Johns <jo...@geek.net>
Committed: Fri Sep 21 20:06:16 2012 +0000

----------------------------------------------------------------------
 Allura/allura/nf/allura/css/allura.css             |    6 +
 Allura/allura/nf/allura/css/site_style.css         |    4 +
 Allura/allura/tests/decorators.py                  |    4 +
 ForgeShortUrl/forgeshorturl/main.py                |  203 +++++++++++++++
 ForgeShortUrl/forgeshorturl/model/shorturl.py      |   30 +++
 ForgeShortUrl/forgeshorturl/templates/add.html     |   19 ++
 ForgeShortUrl/forgeshorturl/templates/index.html   |   69 +++++
 .../forgeshorturl/tests/functional/test.py         |   57 ++++
 ForgeShortUrl/forgeshorturl/widgets/short_url.py   |    8 +
 ForgeShortUrl/setup.py                             |   20 ++
 ForgeShortUrl/test.ini                             |   54 ++++
 run_tests                                          |    1 +
 12 files changed, 475 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/Allura/allura/nf/allura/css/allura.css
----------------------------------------------------------------------
diff --git a/Allura/allura/nf/allura/css/allura.css b/Allura/allura/nf/allura/css/allura.css
index 2896dea..5888fed 100644
--- a/Allura/allura/nf/allura/css/allura.css
+++ b/Allura/allura/nf/allura/css/allura.css
@@ -139,6 +139,9 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 #top_nav .ui-icon-tool-link {
   background-image: url("../images/ext_32.png");
 }
+#top_nav .ui-icon-tool-shorturl {
+  background-image: url("../images/ext_32.png");
+}
 #top_nav .ui-icon-tool-blog {
   background-image: url("../images/blog_32.png");
 }
@@ -173,6 +176,9 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 .big_icon.ui-icon-tool-link {
   background-image: url("../images/ext_48.png");
 }
+.big_icon.ui-icon-tool-shorturl {
+  background-image: url("../images/ext_48.png");
+}
 .big_icon.ui-icon-tool-blog {
   background-image: url("../images/blog_48.png");
 }

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/Allura/allura/nf/allura/css/site_style.css
----------------------------------------------------------------------
diff --git a/Allura/allura/nf/allura/css/site_style.css b/Allura/allura/nf/allura/css/site_style.css
index ccfaccc..1d2ac7d 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -2222,6 +2222,10 @@ div.attachment_thumb .file_type span {
 .ui-icon-tool-link {
   background-repeat: no-repeat;
 }
+.ui-icon-tool-shorturl {
+  background-repeat: no-repeat;
+}
+
 
 .ui-icon-tool-blog {
   background-repeat: no-repeat;

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/Allura/allura/tests/decorators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/decorators.py b/Allura/allura/tests/decorators.py
index 4777f07..6595979 100644
--- a/Allura/allura/tests/decorators.py
+++ b/Allura/allura/tests/decorators.py
@@ -53,6 +53,10 @@ with_wiki = with_tool('test', 'Wiki', 'wiki')
 with_git = with_tool('test', 'Git', 'src-git', 'Git', type='git')
 with_hg = with_tool('test', 'Hg', 'src-hg', 'Mercurial', type='hg')
 with_svn = with_tool('test', 'SVN', 'src', 'SVN')
+with_url = with_tool('test', 'ShortUrl', 'url')
+
+
+
 
 def with_repos(func):
     @wraps(func)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/__init__.py b/ForgeShortUrl/forgeshorturl/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/main.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/main.py b/ForgeShortUrl/forgeshorturl/main.py
new file mode 100644
index 0000000..982d418
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/main.py
@@ -0,0 +1,203 @@
+from tg import expose, validate, redirect, flash, request
+from allura.app import Application, SitemapEntry, DefaultAdminController
+from allura import model as M
+from allura.lib.security import require_access, has_access
+from allura.lib import helpers as h
+from allura.controllers import BaseController
+from allura.lib.widgets import form_fields as ffw
+from webob import exc
+from pylons import c, g
+from formencode import validators
+from formencode.compound import All
+import logging
+from forgeshorturl.model.shorturl import ShortUrl
+from datetime import datetime
+from forgeshorturl.widgets.short_url import CreateShortUrlWidget
+import pylons
+
+
+log = logging.getLogger(__name__)
+
+
+class W:
+    page_list = ffw.PageList()
+    page_size = ffw.PageSize()
+    create_short_url_lightbox = \
+        CreateShortUrlWidget(name='create_short_url',
+                             trigger='#sidebar a.create_short_url')
+
+
+class ForgeShortUrlApp(Application):
+    permissions = ['create ', 'update', 'view_private']
+    searchable = True
+    tool_label = 'URL shortener'
+    default_mount_label = 'URL shortener'
+    default_mount_point = 'url'
+    sitemap = []
+    ordinal = 14
+    installable = False
+    hidden = True
+    icons = {
+        24: 'images/ext_24.png',
+        32: 'images/ext_32.png',
+        48: 'images/ext_48.png'
+    }
+
+    def __init__(self, project, config):
+        Application.__init__(self, project, config)
+        self.root = RootController()
+        self.admin = ShortURLAdminController(self)
+
+    @property
+    @h.exceptionless([], log)
+    def sitemap(self):
+        menu_id = self.config.options.mount_label.title()
+        return [SitemapEntry(menu_id, '.')[self.sidebar_menu()]]
+
+    def sidebar_menu(self):
+        links = []
+        if has_access(c.app, "create"):
+            links += [SitemapEntry('Add Short URL',
+                      c.project.url() +
+                      'admin/' +
+                      self.config.options.mount_point +
+                      '/add/',
+                      ui_icon=g.icons['plus'],
+                      className="create_short_url")]
+        return links
+
+    def admin_menu(self):
+        links = []
+        if has_access(c.app, "create"):
+            links = [SitemapEntry('Add Short URL',
+                                  c.project.url() +
+                                  'admin/' +
+                                  self.config.options.mount_point +
+                                  '/add/',
+                                  className='admin_modal'), ]
+            links += [SitemapEntry('Browse',
+                                   c.project.url() +
+                                   self.config.options.mount_point), ]
+
+        links += super(ForgeShortUrlApp, self).admin_menu()
+        return links
+
+    def install(self, project):
+        'Set up any default permissions and roles here'
+        self.config.options['project_name'] = project.name
+        super(ForgeShortUrlApp, self).install(project)
+        # Setup permissions
+        role_admin = M.ProjectRole.by_name('Admin')._id
+        role_anon = M.ProjectRole.anonymous()._id
+        self.config.acl = [
+            M.ACE.allow(role_admin, 'create'),
+            M.ACE.allow(role_admin, 'update'),
+            M.ACE.allow(role_admin, 'view_private'),
+            M.ACE.allow(role_anon, 'read'),
+            M.ACE.allow(role_admin, 'configure'), ]
+
+    def uninstall(self, project):
+        "Remove all the tool's artifacts from the database"
+        super(ForgeShortUrlApp, self).uninstall(project)
+
+
+class RootController(BaseController):
+    def __init__(self):
+        c.create_short_url_lightbox = W.create_short_url_lightbox
+
+    def _check_security(self):
+        require_access(c.app, 'read')
+
+    @expose('jinja:forgeshorturl:templates/index.html')
+    @validate(dict(page=validators.Int(if_empty=0),
+                   limit=validators.Int(if_empty=100)))
+    def index(self, page=0, limit=100, **kw):
+        c.page_list = W.page_list
+        c.page_size = W.page_size
+        limit, pagenum, start = g.handle_paging(limit, page, default=100)
+        p = {'project_id': c.project._id}
+        if not has_access(c.app, 'view_private'):
+            p['private'] = False
+        short_urls = (ShortUrl.query.find(p))
+        count = short_urls.count()
+
+        short_urls = short_urls.skip(start).limit(limit)
+
+        return {
+            'short_urls': short_urls,
+            'limit': limit,
+            'pagenum': pagenum,
+            'count': count
+        }
+
+    @expose()
+    def _lookup(self, pname, *remainder):
+        if request.method == 'GET':
+            short_url = ShortUrl.query.find({'project_id': c.project._id,
+                                             'short_name': pname}).first()
+            if short_url:
+                u = validators.URL(add_http=True)
+                redirect(u.to_python(short_url.url))
+
+        flash("We're sorry but we weren't able "
+              "to process this request.", "error")
+        raise exc.HTTPNotFound()
+
+
+class ShortURLAdminController(DefaultAdminController):
+    def __init__(self, app):
+        self.app = app
+
+    @expose()
+    def index(self, **kw):
+        redirect(c.project.url() + 'admin/tools')
+
+    @expose('jinja:forgeshorturl:templates/add.html')
+    @validate(dict(full_url=All(validators.URL(),
+                                validators.NotEmpty()),
+                   short_url=validators.NotEmpty()))
+    def add(self, short_url="",
+            full_url="",
+            description="",
+            private="off", **kw):
+        if (request.method == 'POST'):
+            if pylons.c.form_errors:
+                error_msg = "Error creating Short URL: "
+                for msg in list(pylons.c.form_errors):
+                    names = {"short_url": "Short name", "full_url": "Full URL"}
+                    error_msg += "%s - %s " % (names[msg], c.form_errors[msg])
+                    flash(error_msg, "error")
+                redirect(request.referer)
+
+            if (short_url != full_url):
+                shorturl = ShortUrl.query.find({
+                    'short_name': short_url,
+                    'project_id': c.project._id}).first()
+                if shorturl is None:
+                    shorturl = ShortUrl()
+                    shorturl.created = datetime.utcnow()
+                    log_msg = 'create short url %s for %s' %\
+                              (short_url,
+                               full_url)
+                else:
+                    log_msg = 'update short url %s from %s to %s' %\
+                              (short_url,
+                               shorturl.url,
+                               full_url)
+                shorturl.url = full_url
+                shorturl.short_name = short_url
+                shorturl.description = description
+                shorturl.create_user = c.user._id
+                shorturl.project_id = c.project._id
+                if private == "on":
+                    shorturl.private = True
+                else:
+                    shorturl.private = False
+                shorturl.last_updated = datetime.utcnow()
+                M.AuditLog.log(log_msg)
+                flash("Short url created")
+            else:
+                flash("Error creating Short URL: "
+                      "Short Name and Full URL must be different", "error")
+            redirect(request.referer)
+        return dict(app=self.app)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/model/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/model/__init__.py b/ForgeShortUrl/forgeshorturl/model/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/model/shorturl.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/model/shorturl.py b/ForgeShortUrl/forgeshorturl/model/shorturl.py
new file mode 100644
index 0000000..3fefe3d
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/model/shorturl.py
@@ -0,0 +1,30 @@
+from ming.orm.declarative import MappedClass
+from allura.model import project_orm_session
+from ming.orm import FieldProperty, ForeignIdProperty
+from datetime import datetime
+from ming import schema
+from allura.model.auth import User
+from allura.model import Project
+from allura.model.timeline import ActivityNode, ActivityObject
+
+
+class ShortUrl(MappedClass, ActivityNode, ActivityObject):
+
+    class __mongometa__:
+        name = 'short_urs'
+        session = project_orm_session
+        indexes = ['short_name']
+
+    _id = FieldProperty(schema.ObjectId)
+    url = FieldProperty(str)
+    short_name = FieldProperty(str)
+    description = FieldProperty(str)
+    private = FieldProperty(bool)
+    create_user = ForeignIdProperty(User)
+    created = FieldProperty(datetime, if_missing=datetime.utcnow)
+    last_updated = FieldProperty(datetime, if_missing=datetime.utcnow)
+    project_id = ForeignIdProperty(Project)
+
+    @property
+    def user(self):
+        return User.query.get(_id=self.create_user)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/templates/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/templates/__init__.py b/ForgeShortUrl/forgeshorturl/templates/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/templates/add.html
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/templates/add.html b/ForgeShortUrl/forgeshorturl/templates/add.html
new file mode 100644
index 0000000..63f5678
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/templates/add.html
@@ -0,0 +1,19 @@
+{% block content %}
+<div>
+    <form method="post" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/add">
+        <label class="grid-13">Short name</label>
+        <div class="grid-13"><input type = "text" name = "short_url" style="width: 250px"></div>
+        <label class="grid-13">Full URL</label>
+        <div class="grid-13"><input type = "text" name = "full_url" style="width: 250px"></div>
+        <label class="grid-13">Description</label>
+        <div class="grid-13"><textarea name = "description" style="width: 250px; height: 100px"></textarea></div>
+        <div class="grid-1"><input type = "checkbox" name="private" id="private"></div>
+        <label for="private" class="grid-12">Private</label>
+        <div class="grid-13">&nbsp;</div>
+        <hr>
+        <div class="grid-13"><div class="grid-13">&nbsp;</div>
+        <input type="submit" value="Save">
+        <a href="#" class="close">Cancel</a></div>
+    </form>
+</div>
+{% endblock %}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/templates/index.html b/ForgeShortUrl/forgeshorturl/templates/index.html
new file mode 100644
index 0000000..5bf36fb
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/templates/index.html
@@ -0,0 +1,69 @@
+{% extends g.theme.master %}
+
+{% block title %}{{c.project.name}} / {{c.app.config.options.mount_label}} / Link is not configured{% endblock %}
+
+{% block header %}{{c.app.config.options.mount_label}}{% endblock %}
+{% block short_url_content %}{% endblock %}
+{% block content %}
+
+
+<table>
+    <thead>
+    <tr>
+        <th>Private</th>
+        <th>Create user</th>
+        <th>Short URL</th>
+        <th>Full URL</th>
+        <th>Description</th>
+        <th>Created</th>
+        <th>Last updated</th>
+    </tr>
+    </thead>
+    {% for su in short_urls %}
+    <tr>
+        {%if su.private %}
+            <td><small>yes</small></td>
+        {% else %}
+            <td><small>no</small></td>
+        {% endif %}
+
+        <td><small>{{ su.user.username }}</small></td>
+        <td><small><a href="{{ c.app.url+su.short_name }}">{{ request.scheme+'://'+request.host+c.app.url+su.short_name}}</a></small></td>
+        <td><small>{{ su.url|urlize(20) }}</small></td>
+        <td><small>{{ su.description }}</small></td>
+        <td><small>{{ su.created }}</small></td>
+        <td><small>{{ su.last_updated }}</small></td>
+
+    </tr>
+    {% endfor %}
+
+</table>
+{{ c.page_list.display(limit=limit, count=count, page=pagenum) }}
+{% endblock %}
+{% block extra_js %}
+{{c.create_short_url_lightbox.display(content='''
+<div>
+    <h1>Add Short URL</h1>
+    <form method="post" action="{{c.project.url()}}admin/{{app.config.options.mount_point}}/add">
+        <label class="grid-13">Short name</label>
+        <div class="grid-13"><input type = "text" name = "short_url" style="width: 250px"></div>
+        <label class="grid-13">Full URL</label>
+        <div class="grid-13"><input type = "text" name = "full_url" style="width: 250px"></div>
+        <label class="grid-13">Description</label>
+        <div class="grid-13"><textarea name = "description" style="width: 250px; height: 100px"></textarea></div>
+        <div class="grid-1"><input type = "checkbox" name="private" id="private"></div>
+        <label for="private" class="grid-12">Private</label>
+        <div class="grid-13">&nbsp;</div>
+        <hr>
+        <div class="grid-13"><div class="grid-13">&nbsp;</div>
+            <input type="submit" value="Save">
+            <a href="#" class="close">Cancel</a></div>
+    </form>
+</div>
+''')}}
+<script type="text/javascript">
+    /*<![CDATA[*/
+        $('form').attr("action",$('#sidebar a.create_short_url').attr('href'));
+    /*]]>*/
+</script>
+{% endblock %}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/tests/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/tests/__init__.py b/ForgeShortUrl/forgeshorturl/tests/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/tests/functional/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/tests/functional/__init__.py b/ForgeShortUrl/forgeshorturl/tests/functional/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/tests/functional/test.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/tests/functional/test.py b/ForgeShortUrl/forgeshorturl/tests/functional/test.py
new file mode 100644
index 0000000..b143ed6
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/tests/functional/test.py
@@ -0,0 +1,57 @@
+from allura.tests import decorators as td
+from alluratest.controller import TestController
+
+
+class TestRootController(TestController):
+    def setUp(self):
+        super(TestRootController, self).setUp()
+        self.setup_with_tools()
+
+    @td.with_url
+    def setup_with_tools(self):
+        pass
+
+    def test_shorturl_add(self):
+        response = self.app.get('/admin/url/add')
+        response.form['short_url'] = 'test'
+        response.form['full_url'] = 'http://www.google.com/'
+        response.form.submit()
+        redirected = self.app.get('/url/test').follow()
+        assert redirected.request.url == 'http://www.google.com/'
+
+    def test_shorturl_not_found(self):
+        self.app.post('/admin/url/add',
+                      dict(short_url='test',
+                           full_url='http://www.google.com/',
+                           description="description2"))
+        r = self.app.get('/url/test2', status=404)
+        r = self.app.get('/url/')
+        assert 'http://www.google.com/' in r
+
+    def test_shorturl_private(self):
+        d = dict(short_url='test_private',
+                 full_url='http://www.amazone.com/',
+                 private='on',
+                 description="description1")
+        self.app.post('/admin/url/add',
+                      dict(short_url='test_private',
+                           full_url='http://www.amazone.com/',
+                           private='on',
+                           description="description1"))
+
+        self.app.post('/admin/url/add',
+                      dict(short_url='test',
+                           full_url='http://www.google.com/',
+                           description="description2"))
+        r = self.app.get('/url/', extra_environ=dict(username='*anonymous'))
+        assert 'http://www.google.com/' in r
+        assert 'http://www.amazone.com/' not in r
+
+    def test_shorturl_errors(self):
+        d = dict(short_url='http://www.amazone.com/',
+                 full_url='http://www.amazone.com/')
+        r = self.app.post('/admin/url/add', params=d)
+        assert 'error' in self.webflash(r)
+        d = dict(short_url='http://www.amazone.com/', full_url='amazone')
+        r = self.app.post('/admin/url/add', params=d)
+        assert 'error' in self.webflash(r)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/widgets/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/widgets/__init__.py b/ForgeShortUrl/forgeshorturl/widgets/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/forgeshorturl/widgets/short_url.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/widgets/short_url.py b/ForgeShortUrl/forgeshorturl/widgets/short_url.py
new file mode 100644
index 0000000..cc040e8
--- /dev/null
+++ b/ForgeShortUrl/forgeshorturl/widgets/short_url.py
@@ -0,0 +1,8 @@
+from allura.lib.widgets import form_fields as ffw
+
+
+class CreateShortUrlWidget(ffw.Lightbox):
+
+    def resources(self):
+        for r in super(CreateShortUrlWidget, self).resources():
+            yield r

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/setup.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/setup.py b/ForgeShortUrl/setup.py
new file mode 100644
index 0000000..641ae2f
--- /dev/null
+++ b/ForgeShortUrl/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages
+
+
+setup(name='ForgeShortUrl',
+      description="",
+      long_description="",
+      classifiers=[],
+      keywords='',
+      author='',
+      author_email='',
+      url='',
+      license='',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=['Allura', ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [allura]
+      ShortURL=forgeshorturl.main:ForgeShortUrlApp""",)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/ForgeShortUrl/test.ini
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/test.ini b/ForgeShortUrl/test.ini
new file mode 100644
index 0000000..8110f04
--- /dev/null
+++ b/ForgeShortUrl/test.ini
@@ -0,0 +1,54 @@
+#
+# allura - TurboGears 2 testing environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = true
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5000
+
+[app:main]
+use = config:../Allura/test.ini
+
+[app:main_without_authn]
+use = config:../Allura/test.ini#main_without_authn
+
+[app:main_with_amqp]
+use = config:../Allura/test.ini#main_with_amqp
+
+[loggers]
+keys = root, allura, tool
+
+[handlers]
+keys = test
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = test
+
+[logger_allura]
+level = DEBUG
+handlers =
+qualname = allura
+
+[logger_tool]
+level = DEBUG
+handlers =
+qualname = forgeshorturl
+
+[handler_test]
+class = FileHandler
+args = ('test.log',)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/31f5f3ff/run_tests
----------------------------------------------------------------------
diff --git a/run_tests b/run_tests
index b361ee3..decc004 100755
--- a/run_tests
+++ b/run_tests
@@ -23,6 +23,7 @@ if [ "$TEST_MODULES"  == "" ]; then
     ForgeWiki \
     ForgeDownloads \
     ForgeActivity \
+    ForgeShortUrl \
     "
 fi