You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by je...@apache.org on 2015/02/16 12:44:32 UTC

[01/37] allura git commit: [#5726] Rss feed now updates correctly for new topics.

Repository: allura
Updated Branches:
  refs/heads/ib/4542 c17783c25 -> b967bc5cc (forced update)


[#5726] Rss feed now updates correctly for new topics.


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

Branch: refs/heads/ib/4542
Commit: ef5610b98e87ecaed02fa7e3088f33ccfa14c918
Parents: 852360f
Author: Heith Seewald <hs...@slashdotmedia.com>
Authored: Thu Jan 29 17:23:56 2015 -0500
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Mon Feb 2 18:23:51 2015 +0000

----------------------------------------------------------------------
 .../forgediscussion/controllers/root.py         |  3 +-
 .../tests/functional/test_forum.py              | 64 ++++++++++++++++++++
 2 files changed, 66 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/ef5610b9/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index cf1116e..9174fba 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -131,7 +131,8 @@ class RootController(BaseController, DispatchIndex, FeedController):
         require_access(discussion, 'post')
         thd = discussion.get_discussion_thread(dict(
             headers=dict(Subject=subject)))[0]
-        thd.post(subject, text)
+        p = thd.post(subject, text)
+        thd.post_to_feed(p)
         flash('Message posted')
         redirect(thd.url())
 

http://git-wip-us.apache.org/repos/asf/allura/blob/ef5610b9/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
index 34b71c9..3f45b46 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
@@ -27,6 +27,7 @@ from email.mime.multipart import MIMEMultipart
 import pkg_resources
 from pylons import tmpl_context as c
 from nose.tools import assert_equal, assert_in
+import feedparser
 
 from allura import model as M
 from allura.tasks import mail_tasks
@@ -632,6 +633,69 @@ class TestForum(TestController):
         r = self.app.get('/discussion/testforum/childforum/')
         self.check_announcement_table(r, 'AAAA')
 
+    def test_post_to_feed(self):
+        # Create a new topic
+        r = self.app.get('/discussion/create_topic/')
+        f = r.html.find(
+            'form', {'action': '/p/test/discussion/save_new_topic'})
+        params = dict()
+        inputs = f.findAll('input')
+        for field in inputs:
+            if field.has_key('name'):
+                params[field['name']] = field.has_key(
+                    'value') and field['value'] or ''
+        params[f.find('textarea')['name']] = 'XYZ'
+        params[f.find('select')['name']] = 'testforum'
+        params[f.find('input', {'style': 'width: 90%'})['name']] = 'AAAA'
+        thread = self.app.post('/discussion/save_new_topic', params=params).follow()
+        url = thread.request.url
+
+        # Check that the newly created topic is the most recent in the rss feed
+        f = self.app.get('/discussion/feed.rss').body
+        f = feedparser.parse(f)
+        newest_entry = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
+        assert newest_entry == 'XYZ'
+
+        # Reply to the newly created thread.
+        thread = self.app.get(url)
+        t = thread.html.find(
+            'div', {'class': 'row reply_post_form'}).find('form')
+        rep_url = t.get('action')
+        params = dict()
+        inputs = t.findAll('input')
+        for field in inputs:
+            if field.has_key('name'):
+                params[field['name']] = field.has_key(
+                    'value') and field['value'] or ''
+        params[t.find('textarea')['name']] = 'bbb'
+        self.app.post(str(rep_url), params=params)
+
+        # Check that reply matches the newest in the rss feed
+        f = self.app.get('/discussion/feed.rss').body
+        f = feedparser.parse(f)
+        newest_reply = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
+        assert newest_reply == 'bbb'
+
+    def test_post_to_feed(self):
+        r = self.app.get('/discussion/create_topic/')
+        f = r.html.find(
+            'form', {'action': '/p/test/discussion/save_new_topic'})
+        params = dict()
+        inputs = f.findAll('input')
+        for field in inputs:
+            if field.has_key('name'):
+                params[field['name']] = field.has_key(
+                    'value') and field['value'] or ''
+        params[f.find('textarea')['name']] = 'XYZ'
+        params[f.find('select')['name']] = 'testforum'
+        params[f.find('input', {'style': 'width: 90%'})['name']] = 'AAAA'
+        self.app.post('/discussion/save_new_topic', params=params)
+
+        f = self.app.get('/discussion/feed.rss').body
+        f = feedparser.parse(f)
+        newest_entry = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
+        assert newest_entry == 'XYZ'
+
     def test_thread_sticky(self):
         r = self.app.get('/discussion/create_topic/')
         f = r.html.find(


[37/37] allura git commit: [#4542] ticket:728 Add more test + refactoring

Posted by je...@apache.org.
[#4542] ticket:728 Add more test + refactoring


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

Branch: refs/heads/ib/4542
Commit: b967bc5cc4de9d98cd94fba8dfe86986559b08d9
Parents: a433fa9
Author: Igor Bondarenko <je...@gmail.com>
Authored: Mon Feb 16 11:03:17 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 11:03:17 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py   | 50 +++++++++++++++++-------------
 Allura/allura/tests/test_webhooks.py  | 30 ++++++++++++++++++
 Allura/allura/tests/unit/test_repo.py | 42 ++++++++++++++++++++++++-
 Allura/allura/webhooks.py             |  4 +--
 4 files changed, 101 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/b967bc5c/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 20bd365..d8ef050 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -134,12 +134,6 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
                 log.info('Compute last commit info %d: %s', (i + 1), ci._id)
 
     if not all_commits and not new_clone:
-        commits_by_branches = {}
-        commits_by_tags = {}
-        # svn has no branches, so we need __default__ as a fallback to collect
-        # all commits into
-        current_branches = ['__default__']
-        current_tags = []
         for commit in commit_ids:
             new = repo.commit(commit)
             user = User.by_email_address(new.committed.email)
@@ -153,26 +147,13 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
                                        related_nodes=[repo.app_config.project],
                                        tags=['commit', repo.tool.lower()])
 
-            branches, tags = repo.symbolics_for_commit(new)
-            if branches:
-                current_branches = branches
-            if tags:
-                current_tags = tags
-            for b in current_branches:
-                if b not in commits_by_branches.keys():
-                    commits_by_branches[b] = []
-                commits_by_branches[b].append(commit)
-            for t in current_tags:
-                if t not in commits_by_tags.keys():
-                    commits_by_tags[t] = []
-                commits_by_tags[t].append(commit)
-
         from allura.webhooks import RepoPushWebhookSender
+        by_branches, by_tags = _group_commits(repo, commit_ids)
         params = []
-        for b, commits in commits_by_branches.iteritems():
+        for b, commits in by_branches.iteritems():
             ref = u'refs/heads/{}'.format(b) if b != '__default__' else None
             params.append(dict(commit_ids=commits, ref=ref))
-        for t, commits in commits_by_tags.iteritems():
+        for t, commits in by_tags.iteritems():
             ref = u'refs/tags/{}'.format(t)
             params.append(dict(commit_ids=commits, ref=ref))
         if params:
@@ -646,3 +627,28 @@ def _update_tree_cache(tree_ids, cache):
     cached_ids = set(cache.instance_ids(Tree))
     new_ids = current_ids - cached_ids
     cache.batch_load(Tree, {'_id': {'$in': list(new_ids)}})
+
+
+def _group_commits(repo, commit_ids):
+    by_branches = {}
+    by_tags = {}
+    # svn has no branches, so we need __default__ as a fallback to collect
+    # all commits into
+    current_branches = ['__default__']
+    current_tags = []
+    for commit in commit_ids:
+        ci = repo.commit(commit)
+        branches, tags = repo.symbolics_for_commit(ci)
+        if branches:
+            current_branches = branches
+        if tags:
+            current_tags = tags
+        for b in current_branches:
+            if b not in by_branches.keys():
+                by_branches[b] = []
+            by_branches[b].append(commit)
+        for t in current_tags:
+            if t not in by_tags.keys():
+                by_tags[t] = []
+            by_tags[t].append(commit)
+    return by_branches, by_tags

http://git-wip-us.apache.org/repos/asf/allura/blob/b967bc5c/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 629b322..c0f9140 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -485,6 +485,18 @@ class TestRepoPushWebhookSender(TestWebhookBase):
             self.wh._id,
             sender.get_payload.return_value)
 
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send_with_list(self, send_webhook):
+        sender = RepoPushWebhookSender()
+        sender.get_payload = Mock(side_effect=[1, 2])
+        self.wh.enforce_limit = Mock(return_value=True)
+        with h.push_config(c, app=self.git):
+            sender.send([dict(arg1=1, arg2=2), dict(arg1=3, arg2=4)])
+        assert_equal(send_webhook.post.call_count, 2)
+        assert_equal(send_webhook.post.call_args_list,
+                     [call(self.wh._id, 1), call(self.wh._id, 2)])
+        assert_equal(self.wh.enforce_limit.call_count, 1)
+
     @patch('allura.webhooks.log', autospec=True)
     @patch('allura.webhooks.send_webhook', autospec=True)
     def test_send_limit_reached(self, send_webhook, log):
@@ -549,6 +561,24 @@ class TestRepoPushWebhookSender(TestWebhookBase):
             add_webhooks('two', 3)
             assert_equal(sender.enforce_limit(self.git), False)
 
+    def test_before(self):
+        sender = RepoPushWebhookSender()
+        with patch.object(self.git.repo, 'commit', autospec=True) as _ci:
+            assert_equal(sender._before(self.git.repo, ['3', '2', '1']), '')
+            _ci.return_value.parent_ids = ['0']
+            assert_equal(sender._before(self.git.repo, ['3', '2', '1']), '0')
+
+    def test_after(self):
+        sender = RepoPushWebhookSender()
+        assert_equal(sender._after([]), '')
+        assert_equal(sender._after(['3', '2', '1']), '3')
+
+    def test_convert_id(self):
+        sender = RepoPushWebhookSender()
+        assert_equal(sender._convert_id(''), '')
+        assert_equal(sender._convert_id('a433fa9'), 'a433fa9')
+        assert_equal(sender._convert_id('a433fa9:13'), 'r13')
+
 
 class TestModels(TestWebhookBase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/b967bc5c/Allura/allura/tests/unit/test_repo.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/unit/test_repo.py b/Allura/allura/tests/unit/test_repo.py
index f2554fb..7411db5 100644
--- a/Allura/allura/tests/unit/test_repo.py
+++ b/Allura/allura/tests/unit/test_repo.py
@@ -19,13 +19,18 @@ import datetime
 import unittest
 from mock import patch, Mock, MagicMock
 from nose.tools import assert_equal
+from datadiff import tools as dd
 
 from pylons import tmpl_context as c
 
 from allura import model as M
 from allura.controllers.repository import topo_sort
 from allura.model.repository import zipdir, prefix_paths_union
-from allura.model.repo_refresh import CommitRunDoc, CommitRunBuilder
+from allura.model.repo_refresh import (
+    CommitRunDoc,
+    CommitRunBuilder,
+    _group_commits,
+)
 from alluratest.controller import setup_unit_test
 
 
@@ -315,3 +320,38 @@ class TestPrefixPathsUnion(unittest.TestCase):
         a = set(['a1', 'a2', 'a3'])
         b = set(['b1', 'a2/foo', 'b3/foo'])
         self.assertItemsEqual(prefix_paths_union(a, b), ['a2'])
+
+
+class TestGroupCommits(object):
+
+    def setUp(self):
+        self.repo = Mock()
+        self.repo.symbolics_for_commit.return_value = ([], [])
+
+    def test_no_branches(self):
+        b, t = _group_commits(self.repo, ['3', '2', '1'])
+        dd.assert_equal(b, {'__default__': ['3', '2', '1']})
+        dd.assert_equal(t, {})
+
+    def test_branches_and_tags(self):
+        self.repo.symbolics_for_commit.side_effect = [
+            (['master'], ['v1.1']),
+            ([], []),
+            ([], []),
+        ]
+        b, t = _group_commits(self.repo, ['3', '2', '1'])
+        dd.assert_equal(b, {'master': ['3', '2', '1']})
+        dd.assert_equal(t, {'v1.1': ['3', '2', '1']})
+
+    def test_multiple_branches(self):
+        self.repo.symbolics_for_commit.side_effect = [
+            (['master'], ['v1.1']),
+            ([], ['v1.0']),
+            (['test1', 'test2'], []),
+        ]
+        b, t = _group_commits(self.repo, ['3', '2', '1'])
+        dd.assert_equal(b, {'master': ['3', '2'],
+                            'test1': ['1'],
+                            'test2': ['1']})
+        dd.assert_equal(t, {'v1.1': ['3'],
+                            'v1.0': ['2', '1']})

http://git-wip-us.apache.org/repos/asf/allura/blob/b967bc5c/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 03a909d..ba55a89 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -337,7 +337,7 @@ class RepoPushWebhookSender(WebhookSender):
                 return self._convert_id(parents[-1])
         return u''
 
-    def _after(self, repo, commit_ids):
+    def _after(self, commit_ids):
         if len(commit_ids) > 0:
             return self._convert_id(commit_ids[0])
         return u''
@@ -353,7 +353,7 @@ class RepoPushWebhookSender(WebhookSender):
         for ci in commits:
             ci['id'] = self._convert_id(ci['id'])
         before = self._before(app.repo, commit_ids)
-        after = self._after(app.repo, commit_ids)
+        after = self._after(commit_ids)
         payload = {
             'size': len(commits),
             'commits': commits,


[18/37] allura git commit: [#4542] ticket:714 Delete webhooks

Posted by je...@apache.org.
[#4542] ticket:714 Delete webhooks


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

Branch: refs/heads/ib/4542
Commit: ad60782fef61daad65b93dcd7e00c1079468b4d8
Parents: 4e8acf6
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Jan 28 14:56:01 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:48 2015 +0000

----------------------------------------------------------------------
 .../ext/admin/templates/webhooks_list.html      | 35 +++++++++++++++++---
 Allura/allura/webhooks.py                       | 19 +++++++++--
 2 files changed, 47 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/ad60782f/Allura/allura/ext/admin/templates/webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/webhooks_list.html b/Allura/allura/ext/admin/templates/webhooks_list.html
index d591173..f54b7fb 100644
--- a/Allura/allura/ext/admin/templates/webhooks_list.html
+++ b/Allura/allura/ext/admin/templates/webhooks_list.html
@@ -27,17 +27,20 @@
     <p><a href="{{c.app.url}}webhooks/{{ hook.type }}">Create</a></p>
     {% if configured_hooks[hook.type] %}
       <table>
-        {% for h in configured_hooks[hook.type] %}
+        {% for wh in configured_hooks[hook.type] %}
         <tr>
           <td>
-            <a href="{{ h.url() }}">{{ h.hook_url }}</a>
+            <a href="{{ wh.url() }}">{{ wh.hook_url }}</a>
           </td>
           <td>
-            <a href="{{ h.app_config.url() }}">{{ h.app_config.options.mount_label }}</a>
+            <a href="{{ wh.app_config.url() }}">{{ wh.app_config.options.mount_label }}</a>
           </td>
-          <td>{{ h.secret or '' }}</td>
+          <td>{{ wh.secret or '' }}</td>
           <td>
-            <a href="#" title="Delete">
+            <a href="{{c.app.url}}webhooks/{{hook.type}}/delete"
+               class="delete-link"
+               data-id="{{h.really_unicode(wh._id)}}"
+               title="Delete">
               <b data-icon="{{g.icons['delete'].char}}" class="ico {{g.icons['delete'].css}}" title="Delete"></b>
             </a>
           </td>
@@ -47,3 +50,25 @@
     {% endif %}
   {% endfor %}
 {% endblock %}
+
+{% block extra_js %}
+<script type="text/javascript">
+$(function() {
+  $('.delete-link').click(function(e) {
+    e.preventDefault();
+    var id = $(this).attr('data-id');
+    var csrf = $.cookie('_session_id');
+    var data = {'webhook': id, '_session_id': csrf};
+    var url = $(this).attr('href');
+    var $tr = $(this).parents('tr')
+    $.post(url, data, function(data) {
+      if (data['status'] == 'ok') {
+        $tr.remove();
+      } else {
+        console.log(data);
+      }
+    });
+  });
+});
+</script>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/ad60782f/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 8fb62d5..c4c6ca6 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -21,7 +21,7 @@ import hmac
 import hashlib
 
 import requests
-from tg import expose, validate, redirect
+from tg import expose, validate, redirect, flash
 from tg.decorators import with_trailing_slash, without_trailing_slash
 from pylons import tmpl_context as c
 from formencode import validators as fev, schema, Invalid
@@ -87,7 +87,7 @@ class WebhookEditForm(WebhookCreateForm):
 
 class WebhookControllerMeta(type):
     def __call__(cls, sender, *args, **kw):
-        """Decorate the `create` post handler with a validator that references
+        """Decorate post handlers with a validator that references
         the appropriate webhook sender for this controller.
         """
         if hasattr(cls, 'create'):
@@ -136,6 +136,7 @@ class WebhookController(BaseController):
         session(wh).flush(wh)
         M.AuditLog.log('add webhook %s %s %s',
                        wh.type, wh.hook_url, wh.app_config.url())
+        flash('Created successfully', 'ok')
         redirect(c.project.url() + 'admin/webhooks/')
 
     @expose()
@@ -154,8 +155,22 @@ class WebhookController(BaseController):
         M.AuditLog.log('edit webhook %s\n%s => %s\n%s => %s\n%s',
             webhook.type, old_url, url, old_app, app.url(),
             'secret changed' if old_secret != secret else '')
+        flash('Edited successfully', 'ok')
         redirect(c.project.url() + 'admin/webhooks/')
 
+    @expose('json:')
+    @require_post()
+    def delete(self, webhook):
+        form = self.edit_form(self.sender)
+        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):


[35/37] allura git commit: [#4542] ticket:728 Handle case when every commit has no branch (i.e. svn)

Posted by je...@apache.org.
[#4542] ticket:728 Handle case when every commit has no branch (i.e. svn)


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

Branch: refs/heads/ib/4542
Commit: 5e5a19d8405c23a4b05fb9ef1144f325809d776b
Parents: a60ccc9
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 16:53:34 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:42 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/5e5a19d8/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 1dc17ba..20bd365 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -136,7 +136,9 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
     if not all_commits and not new_clone:
         commits_by_branches = {}
         commits_by_tags = {}
-        current_branches = []
+        # svn has no branches, so we need __default__ as a fallback to collect
+        # all commits into
+        current_branches = ['__default__']
         current_tags = []
         for commit in commit_ids:
             new = repo.commit(commit)
@@ -168,7 +170,7 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
         from allura.webhooks import RepoPushWebhookSender
         params = []
         for b, commits in commits_by_branches.iteritems():
-            ref = u'refs/heads/{}'.format(b)
+            ref = u'refs/heads/{}'.format(b) if b != '__default__' else None
             params.append(dict(commit_ids=commits, ref=ref))
         for t, commits in commits_by_tags.iteritems():
             ref = u'refs/tags/{}'.format(t)


[05/37] allura git commit: [#7821] ticket:720 More accurate audit logs when changing user's status

Posted by je...@apache.org.
[#7821] ticket:720 More accurate audit logs when changing user's status


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

Branch: refs/heads/ib/4542
Commit: 09365c951ef223be5594163995df12942e7a4fd0
Parents: 1daebd1
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 5 11:54:08 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Fri Feb 6 20:42:52 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/site_admin.py         |  4 +-
 Allura/allura/lib/plugin.py                     | 44 +++++++++++---------
 .../allura/tests/functional/test_site_admin.py  | 28 +++++++++----
 3 files changed, 47 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/09365c95/Allura/allura/controllers/site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/site_admin.py b/Allura/allura/controllers/site_admin.py
index f150370..a85680b 100644
--- a/Allura/allura/controllers/site_admin.py
+++ b/Allura/allura/controllers/site_admin.py
@@ -524,7 +524,7 @@ class AdminUserDetailsController(object):
         if not user or user.is_anonymous():
             raise HTTPNotFound()
         if status == 'enable' and (user.disabled or user.pending):
-            AuthenticationProvider.get(request).activate_user(user)
+            AuthenticationProvider.get(request).activate_user(user, audit=False)
             AuthenticationProvider.get(request).enable_user(user)
             flash('User enabled')
         elif status == 'disable' and not user.disabled:
@@ -532,7 +532,7 @@ class AdminUserDetailsController(object):
             flash('User disabled')
         elif status == 'pending':
             AuthenticationProvider.get(request).deactivate_user(user)
-            AuthenticationProvider.get(request).enable_user(user)
+            AuthenticationProvider.get(request).enable_user(user, audit=False)
             flash('Set user status to pending')
         redirect(request.referer)
 

http://git-wip-us.apache.org/repos/asf/allura/blob/09365c95/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 5cf1905..f3f90a8 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -185,19 +185,19 @@ class AuthenticationProvider(object):
         '''
         raise NotImplementedError, 'validate_password'
 
-    def disable_user(self, user):
+    def disable_user(self, user, **kw):
         '''Disable user account'''
         raise NotImplementedError, 'disable_user'
 
-    def enable_user(self, user):
+    def enable_user(self, user, **kw):
         '''Enable user account'''
         raise NotImplementedError, 'enable_user'
 
-    def activate_user(self, user):
+    def activate_user(self, user, **kw):
         '''Activate user after registration'''
         raise NotImplementedError, 'activate_user'
 
-    def deactivate_user(self, user):
+    def deactivate_user(self, user, **kw):
         '''Deactivate user (== registation not confirmed)'''
         raise NotImplementedError, 'deactivate_user'
 
@@ -355,25 +355,29 @@ class LocalAuthenticationProvider(AuthenticationProvider):
             raise exc.HTTPUnauthorized()
         return user
 
-    def disable_user(self, user):
+    def disable_user(self, user, **kw):
         user.disabled = True
         session(user).flush(user)
-        h.auditlog_user(u'Account disabled', user=user)
+        if kw.get('audit', True):
+            h.auditlog_user(u'Account disabled', user=user)
 
-    def enable_user(self, user):
+    def enable_user(self, user, **kw):
         user.disabled = False
         session(user).flush(user)
-        h.auditlog_user(u'Account enabled', user=user)
+        if kw.get('audit', True):
+            h.auditlog_user(u'Account enabled', user=user)
 
-    def activate_user(self, user):
+    def activate_user(self, user, **kw):
         user.pending = False
         session(user).flush(user)
-        h.auditlog_user('Account activated', user=user)
+        if kw.get('audit', True):
+            h.auditlog_user('Account activated', user=user)
 
-    def deactivate_user(self, user):
+    def deactivate_user(self, user, **kw):
         user.pending = True
         session(user).flush(user)
-        h.auditlog_user('Account deactivated', user=user)
+        if kw.get('audit', True):
+            h.auditlog_user('Account changed to pending', user=user)
 
     def validate_password(self, user, password):
         return self._validate_password(user, password)
@@ -624,17 +628,17 @@ class LdapAuthenticationProvider(AuthenticationProvider):
     def update_notifications(self, user):
         return LocalAuthenticationProvider(None).update_notifications(user)
 
-    def disable_user(self, user):
-        return LocalAuthenticationProvider(None).disable_user(user)
+    def disable_user(self, user, **kw):
+        return LocalAuthenticationProvider(None).disable_user(user, **kw)
 
-    def enable_user(self, user):
-        return LocalAuthenticationProvider(None).enable_user(user)
+    def enable_user(self, user, **kw):
+        return LocalAuthenticationProvider(None).enable_user(user, **kw)
 
-    def activate_user(self, user):
-        return LocalAuthenticationProvider(None).activate_user(user)
+    def activate_user(self, user, **kw):
+        return LocalAuthenticationProvider(None).activate_user(user, **kw)
 
-    def deactivate_user(self, user):
-        return LocalAuthenticationProvider(None).deactivate_user(user)
+    def deactivate_user(self, user, **kw):
+        return LocalAuthenticationProvider(None).deactivate_user(user, **kw)
 
     def get_last_password_updated(self, user):
         return LocalAuthenticationProvider(None).get_last_password_updated(user)

http://git-wip-us.apache.org/repos/asf/allura/blob/09365c95/Allura/allura/tests/functional/test_site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index 17c23f3..e89b9fd 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -356,7 +356,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'enable')
         form['status'].value = 'disable'
-        r = form.submit()
+        with td.audits('Account disabled', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'User disabled', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, True)
         assert_equal(M.User.by_username('test-user-3').pending, False)
@@ -373,7 +375,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'pending')
         form['status'].value = 'disable'
-        r = form.submit()
+        with td.audits('Account disabled', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'User disabled', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, True)
         assert_equal(M.User.by_username('test-user-3').pending, True)
@@ -390,7 +394,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'disable')
         form['status'].value = 'enable'
-        r = form.submit()
+        with td.audits('Account enabled', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'User enabled', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, False)
         assert_equal(M.User.by_username('test-user-3').pending, False)
@@ -407,7 +413,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'pending')
         form['status'].value = 'enable'
-        r = form.submit()
+        with td.audits('Account enabled', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'User enabled', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, False)
         assert_equal(M.User.by_username('test-user-3').pending, False)
@@ -424,7 +432,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'disable')
         form['status'].value = 'enable'
-        r = form.submit()
+        with td.audits('Account enabled', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'User enabled', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, False)
         assert_equal(M.User.by_username('test-user-3').pending, False)
@@ -441,7 +451,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'disable')
         form['status'].value = 'pending'
-        r = form.submit()
+        with td.audits('Account changed to pending', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'Set user status to pending', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, False)
         assert_equal(M.User.by_username('test-user-3').pending, True)
@@ -458,7 +470,9 @@ class TestUserDetails(TestController):
         assert_equal(form['username'].value, 'test-user-3')
         assert_equal(form['status'].value, 'enable')
         form['status'].value = 'pending'
-        r = form.submit()
+        with td.audits('Account changed to pending', user=True):
+            r = form.submit()
+            assert_equal(M.AuditLog.query.find().count(), 1)
         assert_in(u'Set user status to pending', self.webflash(r))
         assert_equal(M.User.by_username('test-user-3').disabled, False)
         assert_equal(M.User.by_username('test-user-3').pending, True)


[13/37] allura git commit: [#7823] ticket:724 Add tests for new EmailAddress.canonical behavior

Posted by je...@apache.org.
[#7823] ticket:724 Add tests for new EmailAddress.canonical behavior


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

Branch: refs/heads/ib/4542
Commit: 1c2d973c7961195101699270a6dff0e3af644337
Parents: f155a68
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 12 10:28:46 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Fri Feb 13 20:48:35 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/model/test_auth.py | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1c2d973c/Allura/allura/tests/model/test_auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index 3356ce1..12db982 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -74,6 +74,7 @@ def test_email_address():
 @with_setup(setUp)
 def test_email_address_lookup_helpers():
     addr = M.EmailAddress.create('TEST@DOMAIN.NET')
+    nobody = M.EmailAddress.create('nobody@example.com')
     ThreadLocalORMSession.flush_all()
     assert_equal(addr.email, 'TEST@domain.net')
 
@@ -81,14 +82,30 @@ def test_email_address_lookup_helpers():
     assert_equal(M.EmailAddress.get(email='TEST@domain.net'), addr)
     assert_equal(M.EmailAddress.get(email='test@domain.net'), None)
     assert_equal(M.EmailAddress.get(email=None), None)
+    assert_equal(M.EmailAddress.get(email='nobody@example.com'), nobody)
+    # invalid email returns None, but not nobody@example.com as before
+    assert_equal(M.EmailAddress.get(email='invalid'), None)
 
     assert_equal(M.EmailAddress.find(dict(email='TEST@DOMAIN.NET')).all(), [addr])
     assert_equal(M.EmailAddress.find(dict(email='TEST@domain.net')).all(), [addr])
     assert_equal(M.EmailAddress.find(dict(email='test@domain.net')).all(), [])
     assert_equal(M.EmailAddress.find(dict(email=None)).all(), [])
+    assert_equal(M.EmailAddress.find(dict(email='nobody@example.com')).all(), [nobody])
+    # invalid email returns empty query, but not nobody@example.com as before
+    assert_equal(M.EmailAddress.find(dict(email='invalid')).all(), [])
 
 
 @with_setup(setUp)
+def test_email_address_canonical():
+    assert_equal(M.EmailAddress.canonical('nobody@EXAMPLE.COM'),
+                 'nobody@example.com')
+    assert_equal(M.EmailAddress.canonical('nobody@example.com'),
+                 'nobody@example.com')
+    assert_equal(M.EmailAddress.canonical('I Am Nobody <no...@example.com>'),
+                 'nobody@example.com')
+    assert_equal(M.EmailAddress.canonical('invalid'), None)
+
+@with_setup(setUp)
 def test_email_address_send_verification_link():
     addr = M.EmailAddress(email='test_admin@domain.net',
                           claimed_by_user_id=c.user._id)
@@ -178,7 +195,6 @@ def test_user_by_email_address(log):
                            claimed_by_user_id=u1._id)
     addr2 = M.EmailAddress(email='abc123@abc.me', confirmed=True,
                            claimed_by_user_id=u2._id)
-
     # both users are disabled
     u1.disabled, u2.disabled = True, True
     ThreadLocalORMSession.flush_all()
@@ -197,6 +213,14 @@ def test_user_by_email_address(log):
     assert_in(M.User.by_email_address('abc123@abc.me'), [u1, u2])
     assert_equal(log.warn.call_count, 1)
 
+    # invalid email returns None, but not user which claimed
+    # nobody@example.com as before
+    nobody = M.EmailAddress(email='nobody@example.com', confirmed=True,
+                            claimed_by_user_id=u1._id)
+    ThreadLocalORMSession.flush_all()
+    assert_equal(M.User.by_email_address('nobody@example.com'), u1)
+    assert_equal(M.User.by_email_address('invalid'), None)
+
 
 @with_setup(setUp)
 def test_project_role():


[07/37] allura git commit: [#7824] ticket:721 Cache neighborhood record

Posted by je...@apache.org.
[#7824] ticket:721 Cache neighborhood record


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

Branch: refs/heads/ib/4542
Commit: 724ba5951657b96967209a69d7ee9d324c6699b2
Parents: 09365c9
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 5 09:59:15 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 9 07:47:57 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/root.py   |  3 +-
 Allura/allura/lib/app_globals.py    | 34 +++++++++++++++
 Allura/allura/tests/test_globals.py | 74 +++++++++++++++++++++++++++++++-
 Allura/development.ini              |  4 ++
 4 files changed, 113 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/724ba595/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index e588d45..a0418d0 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -24,6 +24,7 @@ from tg import expose, request, config, session
 from tg.decorators import with_trailing_slash
 from tg.flash import TGFlash
 from pylons import tmpl_context as c
+from pylons import app_globals as g
 from paste.deploy.converters import asbool
 
 from allura.app import SitemapEntry
@@ -75,7 +76,7 @@ class RootController(WsgiDispatchController):
 
     def __init__(self):
         n_url_prefix = '/%s/' % request.path.split('/')[1]
-        n = M.Neighborhood.query.get(url_prefix=n_url_prefix)
+        n = g.neighborhood_cache.get(n_url_prefix)
         if n and not n.url_prefix.startswith('//'):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()

http://git-wip-us.apache.org/repos/asf/allura/blob/724ba595/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index d196cc8..f3cb551 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -130,6 +130,36 @@ class ForgeMarkdown(markdown.Markdown):
         return html
 
 
+class NeighborhoodCache(object):
+    """Cached Neighborhood objects by url_prefix.
+    For faster RootController.__init__ lookup
+    """
+
+    def __init__(self, duration):
+        self.duration = duration
+        self._data = {}
+
+    def _lookup(self, url_prefix):
+        n = M.Neighborhood.query.get(url_prefix=url_prefix)
+        self._data[url_prefix] = {
+            'object': n,
+            'ts': datetime.datetime.utcnow(),
+        }
+        return n
+
+    def _expired(self, n):
+        delta = datetime.datetime.utcnow() - n['ts']
+        if delta >= datetime.timedelta(seconds=self.duration):
+            return True
+        return False
+
+    def get(self, url_prefix):
+        n = self._data.get(url_prefix)
+        if n and not self._expired(n):
+            return n['object']
+        return self._lookup(url_prefix)
+
+
 class Globals(object):
 
     """Container for objects available throughout the life of the application.
@@ -256,6 +286,10 @@ class Globals(object):
             macros=_cache_eps('allura.macros'),
         )
 
+        # Neighborhood cache
+        duration = asint(config.get('neighborhood.cache.duration', 0))
+        self.neighborhood_cache = NeighborhoodCache(duration)
+
         # Zarkov logger
         self._zarkov = None
 

http://git-wip-us.apache.org/repos/asf/allura/blob/724ba595/Allura/allura/tests/test_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index dd4e7e6..53c8af1 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -23,6 +23,7 @@ import os
 import allura
 import unittest
 import hashlib
+import datetime as dt
 from mock import patch, Mock
 
 from bson import ObjectId
@@ -41,7 +42,7 @@ from alluratest.controller import (
 
 from allura import model as M
 from allura.lib import helpers as h
-from allura.lib.app_globals import ForgeMarkdown
+from allura.lib.app_globals import ForgeMarkdown, NeighborhoodCache
 from allura.tests import decorators as td
 
 from forgewiki import model as WM
@@ -774,3 +775,74 @@ class TestHandlePaging(unittest.TestCase):
         self.assertEqual(g.handle_paging(None, 2, 30), (25, 2, 50))
         # handle paging must not mess up user preferences
         self.assertEqual(c.user.get_pref('results_per_page'), 25)
+
+
+class TestNeighborhoodCache(object):
+
+    @patch('allura.lib.app_globals.M', autospec=True)
+    @patch('allura.lib.app_globals.datetime', autospec=True)
+    def test_lookup(self, dt_mock, M):
+        dt_mock.datetime.utcnow.side_effect = [
+            dt.datetime(2015, 02, 05, 11, 32),
+            dt.datetime(2015, 02, 05, 11, 34),
+        ]
+        ret = M.Neighborhood.query.get.return_value
+        cache = NeighborhoodCache(30)
+        assert_equal(cache._data, {})
+
+        n = cache._lookup('/p/')
+        M.Neighborhood.query.get.assert_called_once_with(url_prefix='/p/')
+        assert_equal(n, ret)
+        assert_equal(cache._data, {'/p/': {
+            'object': ret,
+            'ts': dt.datetime(2015, 02, 05, 11, 32),
+        }})
+
+        # hits mongo every time
+        n = cache._lookup('/p/')
+        assert_equal(M.Neighborhood.query.get.call_count, 2)
+        assert_equal(n, ret)
+        assert_equal(cache._data, {'/p/': {
+            'object': ret,
+            'ts': dt.datetime(2015, 02, 05, 11, 34),
+        }})
+
+    @patch('allura.lib.app_globals.M', autospec=True)
+    @patch('allura.lib.app_globals.datetime', autospec=True)
+    def test_get(self, dt_mock, M):
+        dt_mock.datetime.utcnow.side_effect = [
+            dt.datetime(2015, 02, 05, 11, 32),
+            dt.datetime(2015, 02, 05, 11, 34),
+        ]
+        ret = M.Neighborhood.query.get.return_value
+        cache = NeighborhoodCache(30)
+        cache._expired = Mock(return_value=False)
+
+        n = cache.get('/p/')
+        M.Neighborhood.query.get.assert_called_once_with(url_prefix='/p/')
+        assert_equal(n, ret)
+
+        # don't hit mongo second time
+        n = cache.get('/p/')
+        assert_equal(M.Neighborhood.query.get.call_count, 1)
+        assert_equal(n, ret)
+
+        # and hits if cache is expired
+        cache._expired.return_value = True
+        n = cache.get('/p/')
+        assert_equal(M.Neighborhood.query.get.call_count, 2)
+        assert_equal(n, ret)
+
+    @patch('allura.lib.app_globals.datetime', autospec=True)
+    def test_expired(self, dt_mock):
+        dt_mock.timedelta = dt.timedelta  # restore original
+        _now = dt.datetime(2015, 02, 05, 11, 53)
+        dt_mock.datetime.utcnow.return_value = _now
+
+        cache = NeighborhoodCache(0)
+        assert_equal(cache._expired({'ts': _now}), True)
+        assert_equal(cache._expired({'ts': _now - dt.timedelta(seconds=1)}), True)
+
+        cache = NeighborhoodCache(30)
+        assert_equal(cache._expired({'ts': _now - dt.timedelta(seconds=29)}), False)
+        assert_equal(cache._expired({'ts': _now - dt.timedelta(seconds=30)}), True)

http://git-wip-us.apache.org/repos/asf/allura/blob/724ba595/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index e5be8b2..c6425a8 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -54,6 +54,10 @@ base_url = http://localhost:8080
 #lang = ru
 cache_dir = %(here)s/data
 
+# Cache Neighborhood objects for N seconds (speeds up requests).
+# Set to 0 to disable (the default).
+# neighborhood.cache.duration = 0
+
 ; Docs at http://beaker.readthedocs.org/en/latest/configuration.html#session-options
 ; and http://beaker.readthedocs.org/en/latest/modules/session.html#beaker.session.CookieSession
 beaker.session.key = allura


[29/37] allura git commit: [#4542] ticket:726 Add edit link and truncate long urls

Posted by je...@apache.org.
[#4542] ticket:726 Add edit link and truncate long urls


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

Branch: refs/heads/ib/4542
Commit: 10cbeeacb88eb8501f9dbcf1a4cb6771a8ab731b
Parents: 8cbd330
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Feb 11 15:54:33 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:40 2015 +0000

----------------------------------------------------------------------
 Allura/allura/templates/app_admin_webhooks_list.html | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/10cbeeac/Allura/allura/templates/app_admin_webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_webhooks_list.html b/Allura/allura/templates/app_admin_webhooks_list.html
index 0fe02e9..a4efb97 100644
--- a/Allura/allura/templates/app_admin_webhooks_list.html
+++ b/Allura/allura/templates/app_admin_webhooks_list.html
@@ -29,10 +29,9 @@
       <table>
         {% for wh in configured_hooks[hook.type] %}
         <tr>
-          <td>
-            <a href="{{ wh.url() }}">{{ wh.hook_url }}</a>
-          </td>
+          <td title="{{wh.hook_url}}">{{ wh.hook_url|truncate(40, True) }}</td>
           <td>{{ wh.secret or '' }}</td>
+          <td><a href="{{wh.url()}}">Edit</a></td>
           <td>
             <a href="{{admin_url}}/{{hook.type}}/delete"
                class="delete-link"


[30/37] allura git commit: [#4542] ticket:728 Use admin_url in Webhook.url()

Posted by je...@apache.org.
[#4542] ticket:728 Use admin_url in Webhook.url()


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

Branch: refs/heads/ib/4542
Commit: a22f6f62282bb3052746870541e79ff1da37772c
Parents: 289eed1
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 13:14:01 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:41 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py           | 2 +-
 Allura/allura/model/webhook.py | 7 +++----
 2 files changed, 4 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/a22f6f62/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index 388b8a2..5a6db3e 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -287,7 +287,7 @@ class Application(object):
     @LazyProperty
     def admin_url(self):
         return '{}{}/{}/'.format(
-            c.project.url(), 'admin',
+            self.project.url(), 'admin',
             self.config.options.mount_point)
 
     @property

http://git-wip-us.apache.org/repos/asf/allura/blob/a22f6f62/Allura/allura/model/webhook.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
index 4c39309..563f871 100644
--- a/Allura/allura/model/webhook.py
+++ b/Allura/allura/model/webhook.py
@@ -36,10 +36,9 @@ class Webhook(Artifact):
     last_sent = FieldProperty(dt.datetime, if_missing=None)
 
     def url(self):
-        return '{}{}/{}/webhooks/{}/{}'.format(
-            self.app_config.project.url(), 'admin',
-            self.app_config.options.mount_point,
-            self.type, self._id)
+        app = self.app_config.load()
+        app = app(self.app_config.project, self.app_config)
+        return '{}webhooks/{}/{}'.format(app.admin_url, self.type, self._id)
 
     def enforce_limit(self):
         '''Returns False if limit is reached, otherwise True'''


[19/37] allura git commit: [#4542] ticket:714 Edit webhooks & validation improvements

Posted by je...@apache.org.
[#4542] ticket:714 Edit webhooks & validation improvements


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

Branch: refs/heads/ib/4542
Commit: 4e8acf6b7bba66b7538565c02d1bdbf251f5e015
Parents: 7d8c47b
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Jan 28 13:40:26 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:48 2015 +0000

----------------------------------------------------------------------
 Allura/allura/ext/admin/admin_main.py           |   2 +-
 .../ext/admin/templates/webhooks_list.html      |   2 +-
 .../allura/templates/webhooks/create_form.html  |  11 +-
 Allura/allura/webhooks.py                       | 115 ++++++++++++++++---
 4 files changed, 106 insertions(+), 24 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/4e8acf6b/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 52b80f9..787bb8c 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -201,7 +201,7 @@ class WebhooksLookup(BaseController):
         webhooks = [ep.load() for ep in webhooks]
         return webhooks
 
-    @with_trailing_slash
+    @without_trailing_slash
     @expose('jinja:allura.ext.admin:templates/webhooks_list.html')
     def index(self):
         webhooks = self._webhooks

http://git-wip-us.apache.org/repos/asf/allura/blob/4e8acf6b/Allura/allura/ext/admin/templates/webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/webhooks_list.html b/Allura/allura/ext/admin/templates/webhooks_list.html
index 39df656..d591173 100644
--- a/Allura/allura/ext/admin/templates/webhooks_list.html
+++ b/Allura/allura/ext/admin/templates/webhooks_list.html
@@ -24,7 +24,7 @@
 {% block content %}
   {% for hook in webhooks %}
     <h1>{{ hook.type }}</h1>
-    <p><a href="{{ hook.type }}">Create</a></p>
+    <p><a href="{{c.app.url}}webhooks/{{ hook.type }}">Create</a></p>
     {% if configured_hooks[hook.type] %}
       <table>
         {% for h in configured_hooks[hook.type] %}

http://git-wip-us.apache.org/repos/asf/allura/blob/4e8acf6b/Allura/allura/templates/webhooks/create_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/webhooks/create_form.html b/Allura/allura/templates/webhooks/create_form.html
index f3eb713..c5ccdfb 100644
--- a/Allura/allura/templates/webhooks/create_form.html
+++ b/Allura/allura/templates/webhooks/create_form.html
@@ -18,9 +18,9 @@
 -#}
 {% extends g.theme.master %}
 
-{% block title %}{{c.project.name}} / Create {{webhook.type}} webhook{% endblock %}
+{% block title %}{{c.project.name}} / {{action|capitalize}} {{sender.type}} webhook{% endblock %}
 
-{% block header %}Create {{webhook.type}} webhook{% endblock %}
+{% block header %}{{action|capitalize}} {{sender.type}} webhook{% endblock %}
 
 {% block extra_css %}
   <style type="text/css">
@@ -64,7 +64,7 @@
 {%- endmacro %}
 
 {% block content %}
-<form action="create" method="post" enctype="multipart/form-data">
+<form action="{{action}}" method="post" enctype="multipart/form-data">
   <div>
     <label for="url">url</label>
     <input name="url" value="{{ c.form_values['url'] }}">
@@ -89,7 +89,10 @@
 
   {% block additional_fields %}{% endblock %}
 
-  <input type="submit" value="Create">
+  <input type="submit" value="{{action|capitalize}}">
+  {% if c.form_values['webhook'] %}
+    <input type="hidden" name="webhook" value="{{c.form_values['webhook']}}">
+  {% endif %}
   {{lib.csrf_token()}}
 </form>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/4e8acf6b/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 9bb99e3..8fb62d5 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -21,15 +21,16 @@ import hmac
 import hashlib
 
 import requests
-from bson import ObjectId
 from tg import expose, validate, redirect
-from tg.decorators import with_trailing_slash
+from tg.decorators import with_trailing_slash, without_trailing_slash
 from pylons import tmpl_context as c
-from formencode import validators as fev, schema
+from formencode import validators as fev, schema, Invalid
 from ming.odm import session
+from webob import exc
 
 from allura.controllers import BaseController
 from allura.lib import helpers as h
+from allura.lib import validators as av
 from allura.lib.decorators import require_post, task
 from allura.lib.utils import DateJSONEncoder
 from allura import model as M
@@ -38,61 +39,139 @@ from allura import model as M
 log = logging.getLogger(__name__)
 
 
+class MingOneOf(av.Ming):
+    def __init__(self, ids, **kw):
+        self.ids = ids
+        super(MingOneOf, self).__init__(**kw)
+
+    def _to_python(self, value, state):
+        result = super(MingOneOf, self)._to_python(value, state)
+        if result and result._id in self.ids:
+            return result
+        raise Invalid(
+            u'Object must be one of: {}, not {}'.format(self.ids, value),
+            value, state)
+
+
+class WebhookValidator(av.Ming):
+    def __init__(self, sender, ac_ids, **kw):
+        self.ac_ids = ac_ids
+        self.sender = sender
+        super(WebhookValidator, self).__init__(cls=M.Webhook, **kw)
+
+    def _to_python(self, value, state):
+        wh = super(WebhookValidator, self)._to_python(value, state)
+        if wh and wh.type == self.sender.type and wh.app_config_id in self.ac_ids:
+            return wh
+        raise Invalid(u'Invalid webhook', value, state)
+
+
 class WebhookCreateForm(schema.Schema):
-    def __init__(self, hook):
+    def __init__(self, sender):
         super(WebhookCreateForm, self).__init__()
         self.triggered_by = [ac for ac in c.project.app_configs
-                             if ac.tool_name.lower() in hook.triggered_by]
-        self.add_field('app', fev.OneOf(
-            [unicode(ac._id) for ac in self.triggered_by]))
+                             if ac.tool_name.lower() in sender.triggered_by]
+        self.add_field('app', MingOneOf(
+            cls=M.AppConfig, ids=[ac._id for ac in self.triggered_by]))
 
     url = fev.URL(not_empty=True)
     secret = fev.UnicodeString()
 
 
+class WebhookEditForm(WebhookCreateForm):
+    def __init__(self, sender):
+        super(WebhookEditForm, self).__init__(sender)
+        self.add_field('webhook', WebhookValidator(
+            sender=sender, ac_ids=[ac._id for ac in self.triggered_by]))
+
+
 class WebhookControllerMeta(type):
-    def __call__(cls, hook, *args, **kw):
+    def __call__(cls, sender, *args, **kw):
         """Decorate the `create` post handler with a validator that references
         the appropriate webhook sender for this controller.
         """
         if hasattr(cls, 'create'):
             cls.create = validate(
-                cls.create_form(hook),
+                cls.create_form(sender),
                 error_handler=cls.index.__func__,
             )(cls.create)
-        return type.__call__(cls, hook, *args, **kw)
+        if hasattr(cls, 'edit'):
+            cls.edit = validate(
+                cls.edit_form(sender),
+                error_handler=cls._default.__func__,
+            )(cls.edit)
+        return type.__call__(cls, sender, *args, **kw)
 
 
 class WebhookController(BaseController):
     __metaclass__ = WebhookControllerMeta
     create_form = WebhookCreateForm
+    edit_form = WebhookEditForm
 
-    def __init__(self, hook):
+    def __init__(self, sender):
         super(WebhookController, self).__init__()
-        self.webhook = hook
+        self.sender = sender
+
+    def gen_secret(self):
+        return h.cryptographic_nonce(20)
 
     @with_trailing_slash
     @expose('jinja:allura:templates/webhooks/create_form.html')
     def index(self, **kw):
-        return {'webhook': self.webhook,
-                'form': self.create_form(self.webhook)}
+        return {'sender': self.sender,
+                'action': 'create',
+                'form': self.create_form(self.sender)}
 
     @expose()
     @require_post()
-    def create(self, url, app, secret=None):
+    def create(self, url, app, secret):
         if not secret:
-            secret = h.cryptographic_nonce(20)
+            secret = self.gen_secret()
         # TODO: catch DuplicateKeyError
         wh = M.Webhook(
             hook_url=url,
             secret=secret,
-            app_config_id=ObjectId(app),
-            type=self.webhook.type)
+            app_config_id=app._id,
+            type=self.sender.type)
         session(wh).flush(wh)
         M.AuditLog.log('add webhook %s %s %s',
                        wh.type, wh.hook_url, wh.app_config.url())
         redirect(c.project.url() + 'admin/webhooks/')
 
+    @expose()
+    @require_post()
+    def edit(self, webhook, url, app, secret):
+        if not secret:
+            secret = self.gen_secret()
+        old_url = webhook.hook_url
+        old_app = webhook.app_config.url()
+        old_secret = webhook.secret
+        webhook.hook_url = url
+        webhook.app_config_id = app._id
+        webhook.secret = secret
+        # TODO: duplicate
+        session(webhook).flush(webhook)
+        M.AuditLog.log('edit webhook %s\n%s => %s\n%s => %s\n%s',
+            webhook.type, old_url, url, old_app, app.url(),
+            'secret changed' if old_secret != secret else '')
+        redirect(c.project.url() + 'admin/webhooks/')
+
+    @without_trailing_slash
+    @expose('jinja:allura:templates/webhooks/create_form.html')
+    def _default(self, webhook, **kw):
+        form = self.edit_form(self.sender)
+        try:
+            wh = form.fields['webhook'].to_python(webhook)
+        except Invalid:
+            raise exc.HTTPNotFound()
+        c.form_values = {'url': kw.get('url') or wh.hook_url,
+                         'app': kw.get('app') or unicode(wh.app_config_id),
+                         'secret': kw.get('secret') or wh.secret,
+                         'webhook': unicode(wh._id)}
+        return {'sender': self.sender,
+                'action': 'edit',
+                'form': form}
+
 
 @task()
 def send_webhook(webhook_id, payload):


[11/37] allura git commit: CHANGES updated for ASF release 1.2.1

Posted by je...@apache.org.
CHANGES updated for ASF release 1.2.1


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

Branch: refs/heads/ib/4542
Commit: 17d427623a30fd9c6986ea137f08a197f3925946
Parents: 1f019a1
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 12:58:29 2015 +0200
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Fri Feb 13 12:58:29 2015 +0200

----------------------------------------------------------------------
 CHANGES | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/17d42762/CHANGES
----------------------------------------------------------------------
diff --git a/CHANGES b/CHANGES
index 6136268..76a4292 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,25 @@
+Version 1.2.1  (February 2015)
+
+Bug Fixes & Minor Improvements
+
+ * [#5726] RSS feed for discussion stopped 12/13/2012? [ss2637]
+ * [#6248] long lines in markdown lists get truncated on the right [ss4073]
+ * [#7772] Type text is splitted in more lines if separated by spaces in bulk edit
+ * [#7813] Handle uppercase in email address all the time
+ * [#7815] KeyError: 'name'
+ * [#7808] Check for wiki presence before importing it
+ * [#7831] Logout issue
+ Administration:
+ * [#7816] Show/manage user's pending status
+ * [#7821] More accurate audit logs when changing user's status
+ Performance:
+ * [#7824] Cache neighborhood record
+ For developers:
+ * [#7516] Timing may case test_set_password_sets_last_updated to fail
+ * [#7795] test_version_race fails occassionally
+ * [#7819] New email address lookup helpers fail on None
+
+
 Version 1.2.0  (December 2014)
 
 Upgrade Instructions


[06/37] allura git commit: [#7824] remove redirect if proj nbhd name differs from nbhd name

Posted by je...@apache.org.
[#7824] remove redirect if proj nbhd name differs from nbhd name

This could potentially cause problems when neighborhood.cache.duration is set.

I believe this redirect harkens back to some early days of Allura where
project shortnames were unique across the system, not per-neighborhood.  So
if a project was accessed via the wrong neighborhood, this would redirect it
to the canonical neighborhood.  But now that's not even possible.


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

Branch: refs/heads/ib/4542
Commit: 70df2e8fe6c121b0ba03a68c34defd3b6aaa0169
Parents: 724ba59
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Fri Feb 6 18:46:28 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 9 07:47:57 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/project.py | 2 --
 1 file changed, 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/70df2e8f/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 8f2b2ef..201ee54 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -129,8 +129,6 @@ class NeighborhoodController(object):
         c.project = project
         if project is None or (project.deleted and not has_access(c.project, 'update')()):
             raise exc.HTTPNotFound, pname
-        if project.neighborhood.name != self.neighborhood_name:
-            redirect(project.url())
         return ProjectController(), remainder
 
     @expose('jinja:allura:templates/neighborhood_project_list.html')


[21/37] allura git commit: [#4542] ticket:714 Add tests for new webhook functionality

Posted by je...@apache.org.
[#4542] ticket:714 Add tests for new webhook functionality


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

Branch: refs/heads/ib/4542
Commit: e7ace573ae8995955fb37de7ec62cd87e9d7bc2e
Parents: ba555ec
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jan 30 15:01:49 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:38 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_utils.py               |   9 +
 Allura/allura/tests/test_webhooks.py            | 469 +++++++++++++++++++
 Allura/allura/webhooks.py                       |   2 +-
 .../forgegit/tests/model/test_repository.py     |  25 +
 .../forgesvn/tests/model/test_repository.py     |  25 +
 5 files changed, 529 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/tests/test_utils.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index e5f9c43..06579c5 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -17,8 +17,10 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+import json
 import time
 import unittest
+import datetime as dt
 from os import path
 
 from webob import Request
@@ -295,3 +297,10 @@ def test_empty_cursor():
     assert_raises(ValueError, cursor.one)
     assert_raises(StopIteration, cursor.next)
     assert_raises(StopIteration, cursor._next_impl)
+
+
+def test_DateJSONEncoder():
+    data = {'message': u'Hi!',
+            'date': dt.datetime(2015, 01, 30, 13, 13, 13)}
+    result = json.dumps(data, cls=utils.DateJSONEncoder)
+    assert_equal(result, '{"date": "2015-01-30T13:13:13Z", "message": "Hi!"}')

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
new file mode 100644
index 0000000..fa0305f
--- /dev/null
+++ b/Allura/allura/tests/test_webhooks.py
@@ -0,0 +1,469 @@
+import json
+import hmac
+import hashlib
+
+from mock import Mock, patch
+from nose.tools import (
+    assert_raises,
+    assert_equal,
+    assert_not_in,
+    assert_in,
+)
+from formencode import Invalid
+from ming.odm import session
+from pylons import tmpl_context as c
+
+from allura import model as M
+from allura.lib import helpers as h
+from allura.lib.utils import DateJSONEncoder
+from allura.webhooks import (
+    MingOneOf,
+    WebhookValidator,
+    WebhookController,
+    send_webhook,
+    RepoPushWebhookSender,
+)
+from allura.tests import decorators as td
+from alluratest.controller import setup_basic_test, TestController
+
+
+# important to be distinct from 'test' and 'test2' which ForgeGit and
+# ForgeImporter use, so that the tests can run in parallel and not clobber each
+# other
+test_project_with_repo = 'adobe-1'
+with_git = td.with_tool(test_project_with_repo, 'git', 'src', 'Git')
+with_git2 = td.with_tool(test_project_with_repo, 'git', 'src2', 'Git2')
+
+
+class TestWebhookBase(object):
+
+    def setUp(self):
+        setup_basic_test()
+        self.setup_with_tools()
+        self.project = M.Project.query.get(shortname=test_project_with_repo)
+        self.git = self.project.app_instance('src')
+        self.wh = M.Webhook(
+            type='repo-push',
+            app_config_id=self.git.config._id,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(self.wh).flush(self.wh)
+
+    @with_git
+    def setup_with_tools(self):
+        pass
+
+
+class TestValidators(TestWebhookBase):
+
+    def test_ming_one_of(self):
+        ids = [ac._id for ac in M.AppConfig.query.find().all()[:2]]
+        v = MingOneOf(cls=M.AppConfig, ids=ids, not_empty=True)
+        with assert_raises(Invalid) as cm:
+            v.to_python(None)
+        assert_equal(cm.exception.msg, u'Please enter a value')
+        with assert_raises(Invalid) as cm:
+            v.to_python('invalid id')
+        assert_equal(cm.exception.msg,
+            u'Object must be one of: %s, not invalid id' % ids)
+        assert_equal(v.to_python(ids[0]), M.AppConfig.query.get(_id=ids[0]))
+        assert_equal(v.to_python(ids[1]), M.AppConfig.query.get(_id=ids[1]))
+        assert_equal(v.to_python(unicode(ids[0])),
+                     M.AppConfig.query.get(_id=ids[0]))
+        assert_equal(v.to_python(unicode(ids[1])),
+                     M.AppConfig.query.get(_id=ids[1]))
+
+    def test_webhook_validator(self):
+        sender = Mock(type='repo-push')
+        ids = [ac._id for ac in M.AppConfig.query.find().all()[:3]]
+        ids, invalid_id = ids[:2], ids[2]
+        v = WebhookValidator(sender=sender, ac_ids=ids, not_empty=True)
+        with assert_raises(Invalid) as cm:
+            v.to_python(None)
+        assert_equal(cm.exception.msg, u'Please enter a value')
+        with assert_raises(Invalid) as cm:
+            v.to_python('invalid id')
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh = M.Webhook(type='invalid type',
+                       app_config_id=invalid_id,
+                       hook_url='http://httpbin.org/post',
+                       secret='secret')
+        session(wh).flush(wh)
+        with assert_raises(Invalid) as cm:
+            v.to_python(wh._id)
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh.type = 'repo-push'
+        session(wh).flush(wh)
+        with assert_raises(Invalid) as cm:
+            v.to_python(wh._id)
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh.app_config_id = ids[0]
+        session(wh).flush(wh)
+        assert_equal(v.to_python(wh._id), wh)
+        assert_equal(v.to_python(unicode(wh._id)), wh)
+
+
+class TestWebhookController(TestController):
+
+    def setUp(self):
+        super(TestWebhookController, self).setUp()
+        self.setup_with_tools()
+        self.patches = self.monkey_patch()
+        for p in self.patches:
+            p.start()
+        self.project = M.Project.query.get(shortname=test_project_with_repo)
+        self.git = self.project.app_instance('src')
+        self.git2 = self.project.app_instance('src2')
+        self.url = str(self.project.url() + 'admin/webhooks')
+
+    def tearDown(self):
+        super(TestWebhookController, self).tearDown()
+        for p in self.patches:
+            p.stop()
+
+    @with_git
+    @with_git2
+    def setup_with_tools(self):
+        pass
+
+    def monkey_patch(self):
+        gen_secret = patch.object(
+            WebhookController,
+            'gen_secret',
+            return_value='super-secret',
+            autospec=True)
+        return [gen_secret]
+
+    def create_webhook(self, data):
+        r = self.app.post(self.url + '/repo-push/create', data)
+        wf = json.loads(self.webflash(r))
+        assert_equal(wf['status'], 'ok')
+        assert_equal(wf['message'], 'Created successfully')
+        return r
+
+    def find_error(self, r, field, msg, form_type='create'):
+        form = r.html.find('form', attrs={'action': form_type})
+        if field == '_the_form':
+            error = form.findPrevious('div', attrs={'class': 'error'})
+        else:
+            widget = 'select' if field == 'app' else 'input'
+            error = form.find(widget, attrs={'name': field})
+            error = error.findNext('div', attrs={'class': 'error'})
+        if error:
+            assert_in(h.escape(msg), error.getText())
+        else:
+            assert False, 'Validation error not found'
+
+    def test_access(self):
+        self.app.get(self.url + '/repo-push/')
+        self.app.get(self.url + '/repo-push/',
+                     extra_environ={'username': 'test-user'},
+                     status=403)
+        r = self.app.get(self.url + '/repo-push/',
+                         extra_environ={'username': '*anonymous'},
+                         status=302)
+        assert_equal(r.location,
+            'http://localhost/auth/'
+            '?return_to=%2Fadobe%2Fadobe-1%2Fadmin%2Fwebhooks%2Frepo-push%2F')
+
+    def test_invalid_hook_type(self):
+        self.app.get(self.url + '/invalid-hook-type/', status=404)
+
+    def test_create(self):
+        assert_equal(M.Webhook.query.find().count(), 0)
+        r = self.app.get(self.url)
+        assert_in('<h1>repo-push</h1>', r)
+        assert_not_in('http://httpbin.org/post', r)
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': ''}
+        msg = 'add webhook repo-push {} {}'.format(
+            data['url'], self.git.config.url())
+        with td.audits(msg):
+            r = self.create_webhook(data).follow().follow(status=200)
+        assert_in('http://httpbin.org/post', r)
+
+        hooks = M.Webhook.query.find().all()
+        assert_equal(len(hooks), 1)
+        assert_equal(hooks[0].type, 'repo-push')
+        assert_equal(hooks[0].hook_url, 'http://httpbin.org/post')
+        assert_equal(hooks[0].app_config_id, self.git.config._id)
+        assert_equal(hooks[0].secret, 'super-secret')
+
+        # Try to create duplicate
+        with td.out_audits(msg):
+            r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, '_the_form',
+            '"repo-push" webhook already exists for Git http://httpbin.org/post')
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+    def test_create_validation(self):
+        assert_equal(M.Webhook.query.find().count(), 0)
+        r = self.app.post(
+            self.url + '/repo-push/create', {}, status=404)
+
+        data = {'url': '', 'app': '', 'secret': ''}
+        r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, 'url', 'Please enter a value')
+        self.find_error(r, 'app', 'Please enter a value')
+
+        data = {'url': 'qwer', 'app': '123', 'secret': 'qwe'}
+        r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, 'url',
+            'You must provide a full domain name (like qwer.com)')
+        self.find_error(r, 'app', 'Object must be one of: ')
+        self.find_error(r, 'app', '%s' % self.git.config._id)
+        self.find_error(r, 'app', '%s' % self.git2.config._id)
+
+    def test_edit(self):
+        data1 = {'url': u'http://httpbin.org/post',
+                 'app': unicode(self.git.config._id),
+                 'secret': u'secret'}
+        data2 = {'url': u'http://example.com/hook',
+                 'app': unicode(self.git2.config._id),
+                 'secret': u'secret2'}
+        self.create_webhook(data1).follow().follow(status=200)
+        self.create_webhook(data2).follow().follow(status=200)
+        assert_equal(M.Webhook.query.find().count(), 2)
+        wh1 = M.Webhook.query.get(hook_url=data1['url'])
+        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
+        form = r.forms[0]
+        assert_equal(form['url'].value, data1['url'])
+        assert_equal(form['app'].value, data1['app'])
+        assert_equal(form['secret'].value, data1['secret'])
+        assert_equal(form['webhook'].value, unicode(wh1._id))
+        form['url'] = 'http://host.org/hook'
+        form['app'] = unicode(self.git2.config._id)
+        form['secret'] = 'new secret'
+        msg = 'edit webhook repo-push\n{} => {}\n{} => {}\n{}'.format(
+            data1['url'], form['url'].value,
+            self.git.config.url(), self.git2.config.url(),
+            'secret changed')
+        with td.audits(msg):
+            r = form.submit()
+        wf = json.loads(self.webflash(r))
+        assert_equal(wf['status'], 'ok')
+        assert_equal(wf['message'], 'Edited successfully')
+        assert_equal(M.Webhook.query.find().count(), 2)
+        wh1 = M.Webhook.query.get(_id=wh1._id)
+        assert_equal(wh1.hook_url, 'http://host.org/hook')
+        assert_equal(wh1.app_config_id, self.git2.config._id)
+        assert_equal(wh1.secret, 'new secret')
+        assert_equal(wh1.type, 'repo-push')
+
+        # Duplicates
+        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
+        form = r.forms[0]
+        form['url'] = data2['url']
+        form['app'] = data2['app']
+        r = form.submit()
+        self.find_error(r, '_the_form',
+            u'"repo-push" webhook already exists for Git2 http://example.com/hook',
+            form_type='edit')
+
+    def test_edit_validation(self):
+        invalid = M.Webhook(
+            type='invalid type',
+            app_config_id=None,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(invalid).flush(invalid)
+        self.app.get(self.url + '/repo-push/%s' % invalid._id, status=404)
+
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': u'secret'}
+        self.create_webhook(data).follow().follow(status=200)
+        wh = M.Webhook.query.get(hook_url=data['url'], type='repo-push')
+
+        # invalid id in hidden field, just in case
+        r = self.app.get(self.url + '/repo-push/%s' % wh._id)
+        data = {k: v[0].value for (k, v) in r.forms[0].fields.items()}
+        data['webhook'] = unicode(invalid._id)
+        self.app.post(self.url + '/repo-push/edit', data, status=404)
+
+        # empty values
+        data = {'url': '', 'app': '', 'secret': '', 'webhook': str(wh._id)}
+        r = self.app.post(self.url + '/repo-push/edit', data)
+        self.find_error(r, 'url', 'Please enter a value', 'edit')
+        self.find_error(r, 'app', 'Please enter a value', 'edit')
+
+        data = {'url': 'qwe', 'app': '123', 'secret': 'qwe',
+                'webhook': str(wh._id)}
+        r = self.app.post(self.url + '/repo-push/edit', data)
+        self.find_error(r, 'url',
+            'You must provide a full domain name (like qwe.com)', 'edit')
+        self.find_error(r, 'app', 'Object must be one of:', 'edit')
+        self.find_error(r, 'app', '%s' % self.git.config._id, 'edit')
+        self.find_error(r, 'app', '%s' % self.git2.config._id, 'edit')
+
+    def test_delete(self):
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': u'secret'}
+        self.create_webhook(data).follow().follow(status=200)
+        assert_equal(M.Webhook.query.find().count(), 1)
+        wh = M.Webhook.query.get(hook_url=data['url'])
+        data = {'webhook': unicode(wh._id)}
+        msg = 'delete webhook repo-push {} {}'.format(
+            wh.hook_url, self.git.config.url())
+        with td.audits(msg):
+            r = self.app.post(self.url + '/repo-push/delete', data)
+        assert_equal(r.json, {'status': 'ok'})
+        assert_equal(M.Webhook.query.find().count(), 0)
+
+    def test_delete_validation(self):
+        invalid = M.Webhook(
+            type='invalid type',
+            app_config_id=None,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(invalid).flush(invalid)
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+        data = {'webhook': ''}
+        self.app.post(self.url + '/repo-push/delete', data, status=404)
+
+        data = {'webhook': unicode(invalid._id)}
+        self.app.post(self.url + '/repo-push/delete', data, status=404)
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+    def test_list_webhooks(self):
+        data1 = {'url': u'http://httpbin.org/post',
+                 'app': unicode(self.git.config._id),
+                 'secret': 'secret'}
+        data2 = {'url': u'http://another-host.org/',
+                 'app': unicode(self.git2.config._id),
+                 'secret': 'secret2'}
+        self.create_webhook(data1).follow().follow(status=200)
+        self.create_webhook(data2).follow().follow(status=200)
+        wh1 = M.Webhook.query.get(hook_url=data1['url'])
+        wh2 = M.Webhook.query.get(hook_url=data2['url'])
+
+        r = self.app.get(self.url)
+        assert_in('<h1>repo-push</h1>', r)
+        rows = r.html.find('table').findAll('tr')
+        assert_equal(len(rows), 2)
+        rows = sorted([self._format_row(row) for row in rows])
+        expected_rows = sorted([
+            [{'href': self.url + '/repo-push/' + str(wh1._id),
+              'text': wh1.hook_url},
+             {'href': self.git.url,
+              'text': self.git.config.options.mount_label},
+             {'text': wh1.secret},
+             {'href': self.url + '/repo-push/delete',
+              'data-id': str(wh1._id)}],
+            [{'href': self.url + '/repo-push/' + str(wh2._id),
+              'text': wh2.hook_url},
+             {'href': self.git2.url,
+              'text': self.git2.config.options.mount_label},
+             {'text': wh2.secret},
+             {'href': self.url + '/repo-push/delete',
+              'data-id': str(wh2._id)}],
+        ])
+        assert_equal(rows, expected_rows)
+
+    def _format_row(self, row):
+        def link(td):
+            a = td.find('a')
+            return {'href': a.get('href'), 'text': a.getText()}
+        def text(td):
+            return {'text': td.getText()}
+        def delete_btn(td):
+            a = td.find('a')
+            return {'href': a.get('href'), 'data-id': a.get('data-id')}
+        tds = row.findAll('td')
+        return [link(tds[0]), link(tds[1]), text(tds[2]), delete_btn(tds[3])]
+
+
+class TestTasks(TestWebhookBase):
+
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send_webhook(self, log, requests):
+        requests.post.return_value = Mock(status_code=200)
+        payload = {'some': ['data']}
+        json_payload = json.dumps(payload, cls=DateJSONEncoder)
+        send_webhook(self.wh._id, payload)
+        signature = hmac.new(
+            self.wh.secret.encode('utf-8'),
+            json_payload.encode('utf-8'),
+            hashlib.sha1)
+        signature = 'sha1=' + signature.hexdigest()
+        headers = {'content-type': 'application/json',
+                   'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
+                   'X-Allura-Signature': signature}
+        requests.post.assert_called_once_with(
+            self.wh.hook_url,
+            data=json_payload,
+            headers=headers,
+            timeout=30)
+        log.info.assert_called_once_with(
+            'Webhook successfully sent: %s %s %s',
+            self.wh.type, self.wh.hook_url, self.wh.app_config.url())
+
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send_webhook_error(self, log, requests):
+        requests.post.return_value = Mock(status_code=500)
+        send_webhook(self.wh._id, {})
+        assert_equal(requests.post.call_count, 1)
+        assert_equal(log.info.call_count, 0)
+        log.error.assert_called_once_with(
+            'Webhook send error: %s %s %s %s %s',
+            self.wh.type, self.wh.hook_url,
+            self.wh.app_config.url(),
+            requests.post.return_value.status_code,
+            requests.post.return_value.reason)
+
+class TestRepoPushWebhookSender(TestWebhookBase):
+
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send(self, send_webhook):
+        sender = RepoPushWebhookSender()
+        sender.get_payload = Mock()
+        with h.push_config(c, app=self.git):
+            sender.send(arg1=1, arg2=2)
+        send_webhook.post.assert_called_once_with(
+            self.wh._id,
+            sender.get_payload.return_value)
+
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send_no_configured_webhooks(self, send_webhook):
+        self.wh.delete()
+        session(self.wh).flush(self.wh)
+        sender = RepoPushWebhookSender()
+        with h.push_config(c, app=self.git):
+            sender.send(arg1=1, arg2=2)
+        assert_equal(send_webhook.post.call_count, 0)
+
+    def test_get_payload(self):
+        sender = RepoPushWebhookSender()
+        _ci = list(range(1, 4))
+        _se = [Mock(info=str(x)) for x in _ci]
+        with patch.object(self.git.repo, 'commit', autospec=True, side_effect=_se):
+            with h.push_config(c, app=self.git):
+                result = sender.get_payload(commit_ids=_ci)
+        expected_result = {
+            'url': 'http://localhost/adobe/adobe-1/src/',
+            'count': 3,
+            'revisions': ['1', '2', '3'],
+        }
+        assert_equal(result, expected_result)
+
+
+class TestModels(TestWebhookBase):
+
+    def test_webhook_find(self):
+        p = M.Project.query.get(shortname='test')
+        assert_equal(M.Webhook.find('smth', p), [])
+        assert_equal(M.Webhook.find('repo-push', p), [])
+        assert_equal(M.Webhook.find('smth', self.project), [])
+        assert_equal(M.Webhook.find('repo-push', self.project), [self.wh])
+
+    def test_webhook_url(self):
+        assert_equal(self.wh.url(),
+            '/adobe/adobe-1/admin/webhooks/repo-push/{}'.format(self.wh._id))

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 8495786..2393acd 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -236,7 +236,7 @@ def send_webhook(webhook_id, payload):
     # TODO: catch
     # TODO: configurable timeout
     r = requests.post(url, data=json_payload, headers=headers, timeout=30)
-    if r.status_code >= 200 and r.status_code <= 300:
+    if r.status_code >= 200 and r.status_code < 300:
         log.info('Webhook successfully sent: %s %s %s',
                  webhook.type, webhook.hook_url, webhook.app_config.url())
     else:

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 4d7a0c0..5e86ab0 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -39,6 +39,7 @@ from allura.tests import decorators as td
 from allura.tests.model.test_repo import RepoImplTestBase
 from allura import model as M
 from allura.model.repo_refresh import send_notifications
+from allura.webhooks import RepoPushWebhookSender
 from forgegit import model as GM
 from forgegit.tests import with_git
 from forgewiki import model as WM
@@ -519,6 +520,30 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
                 self.repo.clone_url('https', 'user'),
                 'https://user@foo.com/')
 
+    def test_webhook_payload(self):
+        sender = RepoPushWebhookSender()
+        cids = list(self.repo.all_commit_ids())[:2]
+        payload = sender.get_payload(commit_ids=cids)
+        expected_payload = {
+            'url': 'http://localhost/p/test/src-git/',
+            'count': 2,
+            'revisions': [
+                {'author': u'Cory Johns',
+                 'author_email': u'cjohns@slashdotmedia.com',
+                 'author_url': None,
+                 'date': datetime.datetime(2013, 3, 28, 18, 54, 16),
+                 'id': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
+                 'shortlink': u'[5c4724]',
+                 'summary': u'Not repo root'},
+                {'author': u'Rick Copeland',
+                 'author_email': u'rcopeland@geek.net',
+                 'author_url': None,
+                 'date': datetime.datetime(2010, 10, 7, 18, 44, 11),
+                 'id': u'1e146e67985dcd71c74de79613719bef7bddca4a',
+                 'shortlink': u'[1e146e]',
+                 'summary': u'Change README'}]}
+        assert_equal(payload, expected_payload)
+
 
 class TestGitImplementation(unittest.TestCase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/ForgeSVN/forgesvn/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 70c34ac..5267614 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -38,6 +38,7 @@ from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 from allura.model.repo_refresh import send_notifications
 from allura.lib import helpers as h
+from allura.webhooks import RepoPushWebhookSender
 from allura.tests.model.test_repo import RepoImplTestBase
 
 from forgesvn import model as SM
@@ -569,6 +570,30 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
             ThreadLocalORMSession.flush_all()
             assert repo2.is_empty()
 
+    def test_webhook_payload(self):
+        sender = RepoPushWebhookSender()
+        cids = list(self.repo.all_commit_ids())[:2]
+        payload = sender.get_payload(commit_ids=cids)
+        expected_payload = {
+            'url': 'http://localhost/p/test/src/',
+            'count': 2,
+            'revisions': [
+                {'author': u'coldmind',
+                 'author_email': u'',
+                 'author_url': None,
+                 'date': datetime(2013, 11, 8, 13, 38, 11, 152000),
+                 'id': u'{}:6'.format(self.repo._id),
+                 'shortlink': '[r6]',
+                 'summary': ''},
+                {'author': u'rick446',
+                 'author_email': u'',
+                 'author_url': None,
+                 'date': datetime(2010, 11, 18, 20, 14, 21, 515000),
+                 'id': u'{}:5'.format(self.repo._id),
+                 'shortlink': '[r5]',
+                 'summary': u'Copied a => b'}]}
+        assert_equal(payload, expected_payload)
+
 
 class TestSVNRev(unittest.TestCase):
 


[17/37] allura git commit: [#4542] ticket:714 Handle DuplicateKeyError

Posted by je...@apache.org.
[#4542] ticket:714 Handle DuplicateKeyError


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

Branch: refs/heads/ib/4542
Commit: ba555ec4743d46dad2bd1c70121d6e437ff202dd
Parents: ad60782
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Jan 29 14:34:19 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:48 2015 +0000

----------------------------------------------------------------------
 .../allura/templates/webhooks/create_form.html  |  1 +
 Allura/allura/webhooks.py                       | 76 ++++++++++++++------
 2 files changed, 55 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/ba555ec4/Allura/allura/templates/webhooks/create_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/webhooks/create_form.html b/Allura/allura/templates/webhooks/create_form.html
index c5ccdfb..6653985 100644
--- a/Allura/allura/templates/webhooks/create_form.html
+++ b/Allura/allura/templates/webhooks/create_form.html
@@ -64,6 +64,7 @@
 {%- endmacro %}
 
 {% block content %}
+{{ error('_the_form') }}
 <form action="{{action}}" method="post" enctype="multipart/form-data">
   <div>
     <label for="url">url</label>

http://git-wip-us.apache.org/repos/asf/allura/blob/ba555ec4/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index c4c6ca6..8495786 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -21,12 +21,14 @@ import hmac
 import hashlib
 
 import requests
+from bson import ObjectId
 from tg import expose, validate, redirect, flash
 from tg.decorators import with_trailing_slash, without_trailing_slash
 from pylons import tmpl_context as c
 from formencode import validators as fev, schema, Invalid
 from ming.odm import session
 from webob import exc
+from pymongo.errors import DuplicateKeyError
 
 from allura.controllers import BaseController
 from allura.lib import helpers as h
@@ -53,14 +55,23 @@ class MingOneOf(av.Ming):
             value, state)
 
 
-class WebhookValidator(av.Ming):
+class WebhookValidator(fev.FancyValidator):
     def __init__(self, sender, ac_ids, **kw):
         self.ac_ids = ac_ids
         self.sender = sender
-        super(WebhookValidator, self).__init__(cls=M.Webhook, **kw)
+        super(WebhookValidator, self).__init__(**kw)
 
     def _to_python(self, value, state):
-        wh = super(WebhookValidator, self)._to_python(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 in self.ac_ids:
             return wh
         raise Invalid(u'Invalid webhook', value, state)
@@ -72,7 +83,9 @@ class WebhookCreateForm(schema.Schema):
         self.triggered_by = [ac for ac in c.project.app_configs
                              if ac.tool_name.lower() in sender.triggered_by]
         self.add_field('app', MingOneOf(
-            cls=M.AppConfig, ids=[ac._id for ac in self.triggered_by]))
+            cls=M.AppConfig,
+            ids=[ac._id for ac in self.triggered_by],
+            not_empty=True))
 
     url = fev.URL(not_empty=True)
     secret = fev.UnicodeString()
@@ -82,7 +95,9 @@ class WebhookEditForm(WebhookCreateForm):
     def __init__(self, sender):
         super(WebhookEditForm, self).__init__(sender)
         self.add_field('webhook', WebhookValidator(
-            sender=sender, ac_ids=[ac._id for ac in self.triggered_by]))
+            sender=sender,
+            ac_ids=[ac._id for ac in self.triggered_by],
+            not_empty=True))
 
 
 class WebhookControllerMeta(type):
@@ -115,9 +130,38 @@ class WebhookController(BaseController):
     def gen_secret(self):
         return h.cryptographic_nonce(20)
 
+    def update_webhook(self, wh, url, ac, secret=None):
+        if not secret:
+            secret = self.gen_secret()
+        wh.hook_url = url
+        wh.app_config_id = ac._id
+        wh.secret = secret
+        try:
+            session(wh).flush(wh)
+        except DuplicateKeyError:
+            session(wh).expunge(wh)
+            msg = u'_the_form: "{}" webhook already exists for {} {}'.format(
+                wh.type, ac.options.mount_label, url)
+            raise Invalid(msg, None, None)
+
+    def form_app_id(self, app):
+        if app and isinstance(app, M.AppConfig):
+            _app = unicode(app._id)
+        elif app:
+            _app = unicode(app)
+        else:
+            _app = None
+        return _app
+
     @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
+            _app = self.form_app_id(kw.get('app'))
+            c.form_values = {'url': kw.get('url'),
+                             'app': _app,
+                             'secret': kw.get('secret')}
         return {'sender': self.sender,
                 'action': 'create',
                 'form': self.create_form(self.sender)}
@@ -125,15 +169,8 @@ class WebhookController(BaseController):
     @expose()
     @require_post()
     def create(self, url, app, secret):
-        if not secret:
-            secret = self.gen_secret()
-        # TODO: catch DuplicateKeyError
-        wh = M.Webhook(
-            hook_url=url,
-            secret=secret,
-            app_config_id=app._id,
-            type=self.sender.type)
-        session(wh).flush(wh)
+        wh = M.Webhook(type=self.sender.type)
+        self.update_webhook(wh, url, app, secret)
         M.AuditLog.log('add webhook %s %s %s',
                        wh.type, wh.hook_url, wh.app_config.url())
         flash('Created successfully', 'ok')
@@ -142,16 +179,10 @@ class WebhookController(BaseController):
     @expose()
     @require_post()
     def edit(self, webhook, url, app, secret):
-        if not secret:
-            secret = self.gen_secret()
         old_url = webhook.hook_url
         old_app = webhook.app_config.url()
         old_secret = webhook.secret
-        webhook.hook_url = url
-        webhook.app_config_id = app._id
-        webhook.secret = secret
-        # TODO: duplicate
-        session(webhook).flush(webhook)
+        self.update_webhook(webhook, url, app, secret)
         M.AuditLog.log('edit webhook %s\n%s => %s\n%s => %s\n%s',
             webhook.type, old_url, url, old_app, app.url(),
             'secret changed' if old_secret != secret else '')
@@ -179,8 +210,9 @@ class WebhookController(BaseController):
             wh = form.fields['webhook'].to_python(webhook)
         except Invalid:
             raise exc.HTTPNotFound()
+        _app = self.form_app_id(kw.get('app')) or unicode(wh.app_config._id)
         c.form_values = {'url': kw.get('url') or wh.hook_url,
-                         'app': kw.get('app') or unicode(wh.app_config_id),
+                         'app': _app,
                          'secret': kw.get('secret') or wh.secret,
                          'webhook': unicode(wh._id)}
         return {'sender': self.sender,


[28/37] allura git commit: [#4542] ticket:726 Move 'Webhooks' from sidebar to app admin

Posted by je...@apache.org.
[#4542] ticket:726 Move 'Webhooks' from sidebar to app admin


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

Branch: refs/heads/ib/4542
Commit: 8cbd33080b76954ea7a0cc55898189d79f3f1f99
Parents: 13a8a60
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Feb 11 14:46:49 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:40 2015 +0000

----------------------------------------------------------------------
 Allura/allura/app.py                            |  56 +++++++++-
 Allura/allura/ext/admin/admin_main.py           |  28 -----
 .../ext/admin/templates/webhooks_list.html      |  74 -------------
 Allura/allura/lib/app_globals.py                |   1 +
 Allura/allura/lib/repository.py                 |   3 -
 Allura/allura/model/webhook.py                  |  15 +--
 .../templates/app_admin_webhooks_list.html      |  71 ++++++++++++
 .../allura/templates/webhooks/create_form.html  |  11 --
 Allura/allura/webhooks.py                       | 108 +++++++------------
 ForgeBlog/forgeblog/main.py                     |   3 -
 ForgeShortUrl/forgeshorturl/main.py             |   3 -
 ForgeTracker/forgetracker/tracker_main.py       |   4 +-
 ForgeWiki/forgewiki/wiki_main.py                |   3 -
 13 files changed, 169 insertions(+), 211 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/app.py
----------------------------------------------------------------------
diff --git a/Allura/allura/app.py b/Allura/allura/app.py
index fff483d..388b8a2 100644
--- a/Allura/allura/app.py
+++ b/Allura/allura/app.py
@@ -30,6 +30,7 @@ from paste.deploy.converters import asbool, asint
 from bson import ObjectId
 from bson.errors import InvalidId
 from formencode import validators as V
+from webob import exc
 
 from ming.orm import session
 from ming.utils import LazyProperty
@@ -40,6 +41,7 @@ from allura import model
 from allura.controllers import BaseController
 from allura.lib.decorators import require_post, memoize
 from allura.lib.utils import permanent_redirect, ConfigProxy
+from allura import model as M
 
 log = logging.getLogger(__name__)
 
@@ -282,6 +284,12 @@ class Application(object):
         """
         return self.config.url(project=self.project)
 
+    @LazyProperty
+    def admin_url(self):
+        return '{}{}/{}/'.format(
+            c.project.url(), 'admin',
+            self.config.options.mount_point)
+
     @property
     def email_address(self):
         """Return email address for this Application.
@@ -524,16 +532,28 @@ class Application(object):
         """
         return ""
 
+    @LazyProperty
+    def _webhooks(self):
+        """A list of webhooks that can be triggered by this app.
+
+        :return: a list of :class:`WebhookSender <allura.webhooks.WebhookSender>`
+        """
+        tool_name = self.config.tool_name.lower()
+        webhooks = [w for w in g.entry_points['webhooks'].itervalues()
+                    if tool_name in w.triggered_by]
+        return webhooks
+
     def admin_menu(self, force_options=False):
         """Return the admin menu for this Application.
 
-        Default implementation will return a menu with up to 3 links:
+        Default implementation will return a menu with up to 4 links:
 
             - 'Permissions', if the current user has admin access to the
                 project in which this Application is installed
             - 'Options', if this Application has custom options, or
                 ``force_options`` is True
             - 'Label', for editing this Application's label
+            - 'Webhooks', if this Application can trigger any webhooks
 
         Subclasses should override this method to provide additional admin
         menu items.
@@ -554,6 +574,8 @@ class Application(object):
                 SitemapEntry('Options', admin_url + 'options', className='admin_modal'))
         links.append(
             SitemapEntry('Label', admin_url + 'edit_label', className='admin_modal'))
+        if len(self._webhooks) > 0:
+            links.append(SitemapEntry('Webhooks', admin_url + 'webhooks'))
         return links
 
     def handle_message(self, topic, message):
@@ -672,7 +694,9 @@ class DefaultAdminController(BaseController):
         """Instantiate this controller for an :class:`app <Application>`.
 
         """
+        super(DefaultAdminController, self).__init__()
         self.app = app
+        self.webhooks = WebhooksLookup(app)
 
     @expose()
     def index(self, **kw):
@@ -858,3 +882,33 @@ class DefaultAdminController(BaseController):
                 if (ace.permission == perm) and (ace.access == model.ACE.DENY):
                     self.app.config.acl.append(ace)
         redirect(request.referer)
+
+
+class WebhooksLookup(BaseController):
+
+    def __init__(self, app):
+        super(WebhooksLookup, self).__init__()
+        self.app = app
+
+    @without_trailing_slash
+    @expose('jinja:allura:templates/app_admin_webhooks_list.html')
+    def index(self):
+        webhooks = self.app._webhooks
+        if len(webhooks) == 0:
+            raise exc.HTTPNotFound()
+        configured_hooks = {}
+        for hook in webhooks:
+            configured_hooks[hook.type] = M.Webhook.query.find({
+                'type': hook.type,
+                'app_config_id': self.app.config._id}
+            ).all()
+        return {'webhooks': webhooks,
+                'configured_hooks': configured_hooks,
+                'admin_url': self.app.admin_url + 'webhooks'}
+
+    @expose()
+    def _lookup(self, name, *remainder):
+        for hook in self.app._webhooks:
+            if hook.type == name and hook.controller:
+                return hook.controller(hook, self.app), remainder
+        raise exc.HTTPNotFound, name

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 787bb8c..b812b0d 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -148,7 +148,6 @@ class AdminApp(Application):
                     SitemapEntry('Categorization', admin_url + 'trove')
                 ]
         links.append(SitemapEntry('Tools', admin_url + 'tools'))
-        links.append(SitemapEntry('Webhooks', admin_url + 'webhooks'))
         if asbool(config.get('bulk_export_enabled', True)):
             links.append(SitemapEntry('Export', admin_url + 'export'))
         if c.project.is_root and has_access(c.project, 'admin')():
@@ -193,32 +192,6 @@ class AdminExtensionLookup(object):
         raise exc.HTTPNotFound, name
 
 
-class WebhooksLookup(BaseController):
-
-    @LazyProperty
-    def _webhooks(self):
-        webhooks = h.iter_entry_points('allura.webhooks')
-        webhooks = [ep.load() for ep in webhooks]
-        return webhooks
-
-    @without_trailing_slash
-    @expose('jinja:allura.ext.admin:templates/webhooks_list.html')
-    def index(self):
-        webhooks = self._webhooks
-        configured_hooks = {}
-        for hook in webhooks:
-            configured_hooks[hook.type] = M.Webhook.find(hook.type, c.project)
-        return {'webhooks': webhooks,
-                'configured_hooks': configured_hooks}
-
-    @expose()
-    def _lookup(self, name, *remainder):
-        for hook in self._webhooks:
-            if hook.type == name and hook.controller:
-                return hook.controller(hook), remainder
-        raise exc.HTTPNotFound, name
-
-
 class ProjectAdminController(BaseController):
     def _check_security(self):
         require_access(c.project, 'admin')
@@ -228,7 +201,6 @@ class ProjectAdminController(BaseController):
         self.groups = GroupsController()
         self.audit = AuditController()
         self.ext = AdminExtensionLookup()
-        self.webhooks = WebhooksLookup()
 
     @with_trailing_slash
     @expose('jinja:allura.ext.admin:templates/project_admin.html')

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/ext/admin/templates/webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/webhooks_list.html b/Allura/allura/ext/admin/templates/webhooks_list.html
deleted file mode 100644
index f54b7fb..0000000
--- a/Allura/allura/ext/admin/templates/webhooks_list.html
+++ /dev/null
@@ -1,74 +0,0 @@
-{#-
-       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.
--#}
-{% extends g.theme.master %}
-
-{% block title %}{{c.project.name}} / Webhooks{% endblock %}
-{% block header %}Webhooks{% endblock %}
-
-{% block content %}
-  {% for hook in webhooks %}
-    <h1>{{ hook.type }}</h1>
-    <p><a href="{{c.app.url}}webhooks/{{ hook.type }}">Create</a></p>
-    {% if configured_hooks[hook.type] %}
-      <table>
-        {% for wh in configured_hooks[hook.type] %}
-        <tr>
-          <td>
-            <a href="{{ wh.url() }}">{{ wh.hook_url }}</a>
-          </td>
-          <td>
-            <a href="{{ wh.app_config.url() }}">{{ wh.app_config.options.mount_label }}</a>
-          </td>
-          <td>{{ wh.secret or '' }}</td>
-          <td>
-            <a href="{{c.app.url}}webhooks/{{hook.type}}/delete"
-               class="delete-link"
-               data-id="{{h.really_unicode(wh._id)}}"
-               title="Delete">
-              <b data-icon="{{g.icons['delete'].char}}" class="ico {{g.icons['delete'].css}}" title="Delete"></b>
-            </a>
-          </td>
-        </tr>
-        {% endfor %}
-      </table>
-    {% endif %}
-  {% endfor %}
-{% endblock %}
-
-{% block extra_js %}
-<script type="text/javascript">
-$(function() {
-  $('.delete-link').click(function(e) {
-    e.preventDefault();
-    var id = $(this).attr('data-id');
-    var csrf = $.cookie('_session_id');
-    var data = {'webhook': id, '_session_id': csrf};
-    var url = $(this).attr('href');
-    var $tr = $(this).parents('tr')
-    $.post(url, data, function(data) {
-      if (data['status'] == 'ok') {
-        $tr.remove();
-      } else {
-        console.log(data);
-      }
-    });
-  });
-});
-</script>
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index f3cb551..fc49749 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -284,6 +284,7 @@ class Globals(object):
             # macro eps are used solely for ensuring that external macros are
             # imported (after load, the ep itself is not used)
             macros=_cache_eps('allura.macros'),
+            webhooks=_cache_eps('allura.webhooks'),
         )
 
         # Neighborhood cache

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/lib/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/repository.py b/Allura/allura/lib/repository.py
index e0b139e..9a4966f 100644
--- a/Allura/allura/lib/repository.py
+++ b/Allura/allura/lib/repository.py
@@ -220,9 +220,6 @@ class RepositoryApp(Application):
 
 class RepoAdminController(DefaultAdminController):
 
-    def __init__(self, app):
-        self.app = app
-
     @LazyProperty
     def repo(self):
         return self.app.repo

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/model/webhook.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
index 09cc7fa..4c39309 100644
--- a/Allura/allura/model/webhook.py
+++ b/Allura/allura/model/webhook.py
@@ -36,17 +36,10 @@ class Webhook(Artifact):
     last_sent = FieldProperty(dt.datetime, if_missing=None)
 
     def url(self):
-        return '{}{}/{}/{}'.format(
-            self.app_config.project.url(),
-            'admin/webhooks',
-            self.type,
-            self._id)
-
-    @classmethod
-    def find(cls, type, project):
-        ac_ids = [ac._id for ac in project.app_configs]
-        hooks = cls.query.find(dict(type=type, app_config_id={'$in': ac_ids}))
-        return hooks.all()
+        return '{}{}/{}/webhooks/{}/{}'.format(
+            self.app_config.project.url(), 'admin',
+            self.app_config.options.mount_point,
+            self.type, self._id)
 
     def enforce_limit(self):
         '''Returns False if limit is reached, otherwise True'''

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/templates/app_admin_webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/app_admin_webhooks_list.html b/Allura/allura/templates/app_admin_webhooks_list.html
new file mode 100644
index 0000000..0fe02e9
--- /dev/null
+++ b/Allura/allura/templates/app_admin_webhooks_list.html
@@ -0,0 +1,71 @@
+{#-
+       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.
+-#}
+{% extends g.theme.master %}
+
+{% block title %}{{c.project.name}} / Webhooks{% endblock %}
+{% block header %}Webhooks{% endblock %}
+
+{% block content %}
+  {% for hook in webhooks %}
+    <h1>{{ hook.type }}</h1>
+    <p><a href="{{admin_url}}/{{hook.type}}">Create</a></p>
+    {% if configured_hooks[hook.type] %}
+      <table>
+        {% for wh in configured_hooks[hook.type] %}
+        <tr>
+          <td>
+            <a href="{{ wh.url() }}">{{ wh.hook_url }}</a>
+          </td>
+          <td>{{ wh.secret or '' }}</td>
+          <td>
+            <a href="{{admin_url}}/{{hook.type}}/delete"
+               class="delete-link"
+               data-id="{{h.really_unicode(wh._id)}}"
+               title="Delete">
+              <b data-icon="{{g.icons['delete'].char}}" class="ico {{g.icons['delete'].css}}" title="Delete"></b>
+            </a>
+          </td>
+        </tr>
+        {% endfor %}
+      </table>
+    {% endif %}
+  {% endfor %}
+{% endblock %}
+
+{% block extra_js %}
+<script type="text/javascript">
+$(function() {
+  $('.delete-link').click(function(e) {
+    e.preventDefault();
+    var id = $(this).attr('data-id');
+    var csrf = $.cookie('_session_id');
+    var data = {'webhook': id, '_session_id': csrf};
+    var url = $(this).attr('href');
+    var $tr = $(this).parents('tr')
+    $.post(url, data, function(data) {
+      if (data['status'] == 'ok') {
+        $tr.remove();
+      } else {
+        console.log(data);
+      }
+    });
+  });
+});
+</script>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/templates/webhooks/create_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/webhooks/create_form.html b/Allura/allura/templates/webhooks/create_form.html
index 6653985..95bfdf6 100644
--- a/Allura/allura/templates/webhooks/create_form.html
+++ b/Allura/allura/templates/webhooks/create_form.html
@@ -72,17 +72,6 @@
     {{ error('url') }}
   </div>
   <div>
-    <label for="app">app</label>
-    <select name="app">
-      {% for ac in form.triggered_by %}
-        <option value="{{ac._id}}"{% if h.really_unicode(ac._id) == c.form_values['app'] %} selected{% endif %}>
-          {{ ac.options.mount_label }}
-        </option>
-      {% endfor %}
-    </select>
-    {{ error('app') }}
-  </div>
-  <div>
     <label for="secret">secret (leave empty to autogenerate)</label>
     <input name="secret" value="{{ c.form_values['secret'] }}">
     {{ error('secret') }}

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 17d7ebf..233c3d5 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -45,23 +45,9 @@ from allura import model as M
 log = logging.getLogger(__name__)
 
 
-class MingOneOf(av.Ming):
-    def __init__(self, ids, **kw):
-        self.ids = ids
-        super(MingOneOf, self).__init__(**kw)
-
-    def _to_python(self, value, state):
-        result = super(MingOneOf, self)._to_python(value, state)
-        if result and result._id in self.ids:
-            return result
-        raise Invalid(
-            u'Object must be one of: {}, not {}'.format(self.ids, value),
-            value, state)
-
-
 class WebhookValidator(fev.FancyValidator):
-    def __init__(self, sender, ac_ids, **kw):
-        self.ac_ids = ac_ids
+    def __init__(self, sender, app, **kw):
+        self.app = app
         self.sender = sender
         super(WebhookValidator, self).__init__(**kw)
 
@@ -76,50 +62,39 @@ class WebhookValidator(fev.FancyValidator):
                 wh = M.Webhook.query.get(_id=ObjectId(value))
             except:
                 pass
-        if wh and wh.type == self.sender.type and wh.app_config_id in self.ac_ids:
+        if wh and wh.type == self.sender.type and wh.app_config_id == self.app.config._id:
             return wh
         raise Invalid(u'Invalid webhook', value, state)
 
 
 class WebhookCreateForm(schema.Schema):
-    def __init__(self, sender):
-        super(WebhookCreateForm, self).__init__()
-        self.triggered_by = [ac for ac in c.project.app_configs
-                             if ac.tool_name.lower() in sender.triggered_by]
-        self.add_field('app', MingOneOf(
-            cls=M.AppConfig,
-            ids=[ac._id for ac in self.triggered_by],
-            not_empty=True))
-
     url = fev.URL(not_empty=True)
     secret = fev.UnicodeString()
 
 
 class WebhookEditForm(WebhookCreateForm):
-    def __init__(self, sender):
-        super(WebhookEditForm, self).__init__(sender)
+    def __init__(self, sender, app):
+        super(WebhookEditForm, self).__init__()
         self.add_field('webhook', WebhookValidator(
-            sender=sender,
-            ac_ids=[ac._id for ac in self.triggered_by],
-            not_empty=True))
+            sender=sender, app=app, not_empty=True))
 
 
 class WebhookControllerMeta(type):
-    def __call__(cls, sender, *args, **kw):
+    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(sender),
+                cls.create_form(),
                 error_handler=cls.index.__func__,
             )(cls.create)
         if hasattr(cls, 'edit'):
             cls.edit = validate(
-                cls.edit_form(sender),
+                cls.edit_form(sender, app),
                 error_handler=cls._default.__func__,
             )(cls.edit)
-        return type.__call__(cls, sender, *args, **kw)
+        return type.__call__(cls, sender, app, *args, **kw)
 
 
 class WebhookController(BaseController):
@@ -127,80 +102,71 @@ class WebhookController(BaseController):
     create_form = WebhookCreateForm
     edit_form = WebhookEditForm
 
-    def __init__(self, sender):
+    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, ac, secret=None):
+    def update_webhook(self, wh, url, secret=None):
         if not secret:
             secret = self.gen_secret()
         wh.hook_url = url
-        wh.app_config_id = ac._id
         wh.secret = secret
         try:
             session(wh).flush(wh)
         except DuplicateKeyError:
             session(wh).expunge(wh)
             msg = u'_the_form: "{}" webhook already exists for {} {}'.format(
-                wh.type, ac.options.mount_label, url)
+                wh.type, self.app.config.options.mount_label, url)
             raise Invalid(msg, None, None)
 
-    def form_app_id(self, app):
-        if app and isinstance(app, M.AppConfig):
-            _app = unicode(app._id)
-        elif app:
-            _app = unicode(app)
-        else:
-            _app = None
-        return _app
-
     @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
-            _app = self.form_app_id(kw.get('app'))
             c.form_values = {'url': kw.get('url'),
-                             'app': _app,
                              'secret': kw.get('secret')}
         return {'sender': self.sender,
                 'action': 'create',
-                'form': self.create_form(self.sender)}
+                'form': self.create_form()}
 
     @expose()
     @require_post()
-    def create(self, url, app, secret):
-        if self.sender.enforce_limit(app):
-            wh = M.Webhook(type=self.sender.type)
-            self.update_webhook(wh, url, app, secret)
+    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',
-                           wh.type, wh.hook_url, wh.app_config.url())
+                           webhook.type, webhook.hook_url,
+                           webhook.app_config.url())
             flash('Created successfully', 'ok')
         else:
-            flash('You have exceeded the maximum number of projects '
+            flash('You have exceeded the maximum number of webhooks '
                   'you are allowed to create for this project/app', 'error')
-        redirect(c.project.url() + 'admin/webhooks/')
+        redirect(self.app.admin_url + 'webhooks')
 
     @expose()
     @require_post()
-    def edit(self, webhook, url, app, secret):
+    def edit(self, webhook, url, secret):
         old_url = webhook.hook_url
-        old_app = webhook.app_config.url()
         old_secret = webhook.secret
-        self.update_webhook(webhook, url, app, secret)
-        M.AuditLog.log('edit webhook %s\n%s => %s\n%s => %s\n%s',
-            webhook.type, old_url, url, old_app, app.url(),
-            'secret changed' if old_secret != secret else '')
+        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(c.project.url() + 'admin/webhooks/')
+        redirect(self.app.admin_url + 'webhooks')
 
     @expose('json:')
     @require_post()
     def delete(self, webhook):
-        form = self.edit_form(self.sender)
+        form = self.edit_form(self.sender, self.app)
         try:
             wh = form.fields['webhook'].to_python(webhook)
         except Invalid:
@@ -213,14 +179,12 @@ class WebhookController(BaseController):
     @without_trailing_slash
     @expose('jinja:allura:templates/webhooks/create_form.html')
     def _default(self, webhook, **kw):
-        form = self.edit_form(self.sender)
+        form = self.edit_form(self.sender, self.app)
         try:
             wh = form.fields['webhook'].to_python(webhook)
         except Invalid:
             raise exc.HTTPNotFound()
-        _app = self.form_app_id(kw.get('app')) or unicode(wh.app_config._id)
         c.form_values = {'url': kw.get('url') or wh.hook_url,
-                         'app': _app,
                          'secret': kw.get('secret') or wh.secret,
                          'webhook': unicode(wh._id)}
         return {'sender': self.sender,
@@ -336,7 +300,7 @@ class WebhookSender(object):
                 else:
                     log.warn('Webhook fires too often: %s. Skipping', webhook)
 
-    def enforce_limit(self, app_config):
+    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.
@@ -344,10 +308,10 @@ class WebhookSender(object):
         _type = self.type.replace('-', '_')
         limits = json.loads(config.get('webhook.%s.max_hooks' % _type, '{}'))
         count = M.Webhook.query.find(dict(
-            app_config_id=app_config._id,
+            app_config_id=app.config._id,
             type=self.type,
         )).count()
-        return count < limits.get(app_config.tool_name.lower(), 3)
+        return count < limits.get(app.config.tool_name.lower(), 3)
 
 
 class RepoPushWebhookSender(WebhookSender):

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/ForgeBlog/forgeblog/main.py
----------------------------------------------------------------------
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index 1e3f604..93cd01b 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -395,9 +395,6 @@ class PostController(BaseController, FeedController):
 
 class BlogAdminController(DefaultAdminController):
 
-    def __init__(self, app):
-        self.app = app
-
     @without_trailing_slash
     @expose('jinja:forgeblog:templates/blog/admin_options.html')
     def options(self):

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/ForgeShortUrl/forgeshorturl/main.py
----------------------------------------------------------------------
diff --git a/ForgeShortUrl/forgeshorturl/main.py b/ForgeShortUrl/forgeshorturl/main.py
index f4ab4ed..b259e92 100644
--- a/ForgeShortUrl/forgeshorturl/main.py
+++ b/ForgeShortUrl/forgeshorturl/main.py
@@ -198,9 +198,6 @@ class ShortURLAdminController(DefaultAdminController):
         )
     )
 
-    def __init__(self, app):
-        self.app = app
-
     @expose()
     def index(self, **kw):
         redirect(c.project.url() + 'admin/tools')

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/ForgeTracker/forgetracker/tracker_main.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index b38bf1d..a7c4e61 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -1528,8 +1528,8 @@ NONALNUM_RE = re.compile(r'\W+')
 class TrackerAdminController(DefaultAdminController):
 
     def __init__(self, app):
-        self.app = app
-        self.bins = BinController(app=app)
+        super(TrackerAdminController, self).__init__(app)
+        self.bins = BinController(app=self.app)
         # if self.app.globals and self.app.globals.milestone_names is None:
         #     self.app.globals.milestone_names = ''
 

http://git-wip-us.apache.org/repos/asf/allura/blob/8cbd3308/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index 3341377..ecbddef 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -801,9 +801,6 @@ class PageRestController(BaseController):
 
 class WikiAdminController(DefaultAdminController):
 
-    def __init__(self, app):
-        self.app = app
-
     def _check_security(self):
         require_access(self.app, 'configure')
 


[15/37] allura git commit: [#4542] ticket:714 Sign webhook payload

Posted by je...@apache.org.
[#4542] ticket:714 Sign webhook payload


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

Branch: refs/heads/ib/4542
Commit: 0f529d6c95393561b2c801a5e6d8bc555e5a4ab5
Parents: 0eb35a9
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Jan 28 11:30:33 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:47 2015 +0000

----------------------------------------------------------------------
 .../allura/ext/admin/templates/webhooks_list.html  |  1 +
 Allura/allura/model/webhook.py                     |  1 +
 Allura/allura/templates/webhooks/create_form.html  |  5 +++++
 Allura/allura/webhooks.py                          | 17 +++++++++++++++--
 4 files changed, 22 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/0f529d6c/Allura/allura/ext/admin/templates/webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/webhooks_list.html b/Allura/allura/ext/admin/templates/webhooks_list.html
index 9bb2476..39df656 100644
--- a/Allura/allura/ext/admin/templates/webhooks_list.html
+++ b/Allura/allura/ext/admin/templates/webhooks_list.html
@@ -35,6 +35,7 @@
           <td>
             <a href="{{ h.app_config.url() }}">{{ h.app_config.options.mount_label }}</a>
           </td>
+          <td>{{ h.secret or '' }}</td>
           <td>
             <a href="#" title="Delete">
               <b data-icon="{{g.icons['delete'].char}}" class="ico {{g.icons['delete'].css}}" title="Delete"></b>

http://git-wip-us.apache.org/repos/asf/allura/blob/0f529d6c/Allura/allura/model/webhook.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
index 73b9540..05eb436 100644
--- a/Allura/allura/model/webhook.py
+++ b/Allura/allura/model/webhook.py
@@ -27,6 +27,7 @@ class Webhook(Artifact):
 
     type = FieldProperty(str)
     hook_url = FieldProperty(str)
+    secret = FieldProperty(str)
 
     def url(self):
         return '{}{}/{}/{}'.format(

http://git-wip-us.apache.org/repos/asf/allura/blob/0f529d6c/Allura/allura/templates/webhooks/create_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/webhooks/create_form.html b/Allura/allura/templates/webhooks/create_form.html
index a44bb5b..f3eb713 100644
--- a/Allura/allura/templates/webhooks/create_form.html
+++ b/Allura/allura/templates/webhooks/create_form.html
@@ -81,6 +81,11 @@
     </select>
     {{ error('app') }}
   </div>
+  <div>
+    <label for="secret">secret (leave empty to autogenerate)</label>
+    <input name="secret" value="{{ c.form_values['secret'] }}">
+    {{ error('secret') }}
+  </div>
 
   {% block additional_fields %}{% endblock %}
 

http://git-wip-us.apache.org/repos/asf/allura/blob/0f529d6c/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index c5dc37e..5c792be 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -17,6 +17,8 @@
 
 import logging
 import json
+import hmac
+import hashlib
 
 import requests
 from bson import ObjectId
@@ -45,6 +47,7 @@ class WebhookCreateForm(schema.Schema):
             [unicode(ac._id) for ac in self.triggered_by]))
 
     url = fev.URL(not_empty=True)
+    secret = fev.UnicodeString()
 
 
 class WebhookControllerMeta(type):
@@ -76,10 +79,13 @@ class WebhookController(BaseController):
 
     @expose()
     @require_post()
-    def create(self, url, app):
+    def create(self, url, app, secret=None):
+        if not secret:
+            secret = h.cryptographic_nonce(20)
         # TODO: catch DuplicateKeyError
         wh = M.Webhook(
             hook_url=url,
+            secret=secret,
             app_config_id=ObjectId(app),
             type=self.webhook.type)
         session(wh).flush(wh)
@@ -90,8 +96,15 @@ class WebhookController(BaseController):
 def send_webhook(webhook_id, payload):
     webhook = M.Webhook.query.get(_id=webhook_id)
     url = webhook.hook_url
-    headers = {'content-type': 'application/json'}
     json_payload = json.dumps(payload, cls=DateJSONEncoder)
+    signature = hmac.new(
+        webhook.secret.encode('utf-8'),
+        json_payload.encode('utf-8'),
+        hashlib.sha1)
+    signature = 'sha1=' + signature.hexdigest()
+    headers = {'content-type': 'application/json',
+               'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
+               'X-Allura-Signature': signature}
     # TODO: catch
     # TODO: configurable timeout
     r = requests.post(url, data=json_payload, headers=headers, timeout=30)


[20/37] allura git commit: [#4542] ticket:714 Audit log of webhooks added

Posted by je...@apache.org.
[#4542] ticket:714 Audit log of webhooks added


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

Branch: refs/heads/ib/4542
Commit: 7d8c47bac3efbc322e98da47b952f6812ee41f1b
Parents: 0f529d6
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Jan 28 11:37:06 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:48 2015 +0000

----------------------------------------------------------------------
 Allura/allura/webhooks.py | 2 ++
 1 file changed, 2 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/7d8c47ba/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 5c792be..9bb99e3 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -89,6 +89,8 @@ class WebhookController(BaseController):
             app_config_id=ObjectId(app),
             type=self.webhook.type)
         session(wh).flush(wh)
+        M.AuditLog.log('add webhook %s %s %s',
+                       wh.type, wh.hook_url, wh.app_config.url())
         redirect(c.project.url() + 'admin/webhooks/')
 
 


[12/37] allura git commit: [#7823] ticket:724 Change EmailAddress.canonical behavior on invalid emails

Posted by je...@apache.org.
[#7823] ticket:724 Change EmailAddress.canonical behavior on invalid emails

Return None, instead of 'nobody@example.com'.


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

Branch: refs/heads/ib/4542
Commit: 0d6a44318b399203a9e732d276b6ebf873e36d8c
Parents: 1c2d973
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 12 13:10:32 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Fri Feb 13 20:48:35 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py | 32 +++++++++++++++-------------
 Allura/allura/lib/utils.py        | 38 +++++++++++++++++++++++++++++++++-
 Allura/allura/model/auth.py       | 28 +++++++++++++++++--------
 Allura/allura/tests/test_utils.py | 19 ++++++++++++++++-
 4 files changed, 92 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/0d6a4431/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 453ad8a..2183032 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -232,7 +232,8 @@ class AuthController(BaseController):
         user.set_tool_data('allura', pwd_reset_preserve_session=session.id)  # else the first password set causes this session to be invalidated
         if require_email:
             em = user.claim_address(email)
-            em.send_verification_link()
+            if em:
+                em.send_verification_link()
             flash('User "%s" registered. Verification link was sent to your email.' % username)
         else:
             plugin.AuthenticationProvider.get(request).login(user)
@@ -490,22 +491,25 @@ class PreferencesController(BaseController):
 
             elif mail_util.isvalid(new_addr['addr']):
                 em = M.EmailAddress.create(new_addr['addr'])
-                user.email_addresses.append(em.email)
-                em.claimed_by_user_id = user._id
-
-                confirmed_emails = filter(lambda email: email.confirmed, claimed_emails)
-                if not confirmed_emails:
-                    if not admin:
-                        em.send_verification_link()
+                if em:
+                    user.email_addresses.append(em.email)
+                    em.claimed_by_user_id = user._id
+
+                    confirmed_emails = filter(lambda email: email.confirmed, claimed_emails)
+                    if not confirmed_emails:
+                        if not admin:
+                            em.send_verification_link()
+                        else:
+                            AuthController()._verify_addr(em)
                     else:
-                        AuthController()._verify_addr(em)
-                else:
-                    em.send_claim_attempt()
+                        em.send_claim_attempt()
 
-                if not admin:
-                    flash('A verification email has been sent.  Please check your email and click to confirm.')
+                    if not admin:
+                        flash('A verification email has been sent.  Please check your email and click to confirm.')
 
-                h.auditlog_user('New email address: %s', new_addr['addr'], user=user)
+                    h.auditlog_user('New email address: %s', new_addr['addr'], user=user)
+                else:
+                    flash('Email address %s is invalid' % new_addr['addr'], 'error')
             else:
                 flash('Email address %s is invalid' % new_addr['addr'], 'error')
         if not primary_addr and not user.get_pref('email_address') and user.email_addresses:

http://git-wip-us.apache.org/repos/asf/allura/blob/0d6a4431/Allura/allura/lib/utils.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index 35bc039..7a56603 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -48,6 +48,7 @@ import html5lib.sanitizer
 
 from ew import jinja2_ew as ew
 from ming.utils import LazyProperty
+from ming.odm.odmsession import ODMCursor
 
 
 MARKDOWN_EXTENSIONS = ['.markdown', '.mdown', '.mkdn', '.mkd', '.md']
@@ -555,4 +556,39 @@ def ip_address(request):
     ip = request.remote_addr
     if tg.config.get('ip_address_header'):
         ip = request.headers.get(tg.config['ip_address_header']) or ip
-    return ip
\ No newline at end of file
+    return ip
+
+
+class EmptyCursor(ODMCursor):
+    """Ming cursor with no results"""
+
+    def __init__(self, *args, **kw):
+        pass
+
+    @property
+    def extensions(self):
+        return []
+
+    def count(self):
+        return 0
+
+    def _next_impl(self):
+        raise StopIteration
+
+    def next(self):
+        raise StopIteration
+
+    def options(self, **kw):
+        return self
+
+    def limit(self, limit):
+        return self
+
+    def skip(self, skip):
+        return self
+
+    def hint(self, index_or_name):
+        return self
+
+    def sort(self, *args, **kw):
+        return self

http://git-wip-us.apache.org/repos/asf/allura/blob/0d6a4431/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index e8bd8a6..0b92443 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -128,7 +128,11 @@ class EmailAddress(MappedClass):
         '''Equivalent to Ming's query.get but calls self.canonical on address
         before lookup. You should always use this instead of query.get'''
         if kw.get('email'):
-            kw['email'] = cls.canonical(kw['email'])
+            email = cls.canonical(kw['email'])
+            if email is not None:
+                kw['email'] = email
+            else:
+                return None
         return cls.query.get(**kw)
 
     @classmethod
@@ -137,7 +141,11 @@ class EmailAddress(MappedClass):
         before lookup. You should always use this instead of query.find'''
         if q:
             if q.get('email'):
-                q['email'] = cls.canonical(q['email'])
+                email = cls.canonical(q['email'])
+                if email is not None:
+                    q['email'] = email
+                else:
+                    return utils.EmptyCursor()
             return cls.query.find(q)
         return cls.query.find()
 
@@ -152,7 +160,8 @@ class EmailAddress(MappedClass):
     @classmethod
     def create(cls, addr):
         addr = cls.canonical(addr)
-        return cls(email=addr)
+        if addr is not None:
+            return cls(email=addr)
 
     @classmethod
     def canonical(cls, addr):
@@ -163,7 +172,7 @@ class EmailAddress(MappedClass):
             user, domain = addr.split('@')
             return '%s@%s' % (user, domain.lower())
         else:
-            return 'nobody@example.com'
+            return None
 
     def send_claim_attempt(self):
         confirmed_email = self.find(dict(email=self.email, confirmed=True)).all()
@@ -666,11 +675,12 @@ class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     def claim_address(self, email_address):
         addr = EmailAddress.canonical(email_address)
         email_addr = EmailAddress.create(addr)
-        email_addr.claimed_by_user_id = self._id
-        if addr not in self.email_addresses:
-            self.email_addresses.append(addr)
-        session(email_addr).flush(email_addr)
-        return email_addr
+        if email_addr:
+            email_addr.claimed_by_user_id = self._id
+            if addr not in self.email_addresses:
+                self.email_addresses.append(addr)
+            session(email_addr).flush(email_addr)
+            return email_addr
 
     @classmethod
     def register(cls, doc, make_project=True):

http://git-wip-us.apache.org/repos/asf/allura/blob/0d6a4431/Allura/allura/tests/test_utils.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index 209d0df..e5f9c43 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -23,7 +23,7 @@ from os import path
 
 from webob import Request
 from mock import Mock, patch
-from nose.tools import assert_equal
+from nose.tools import assert_equal, assert_raises
 from pygments import highlight
 from pygments.lexers import get_lexer_for_filename
 from tg import config
@@ -278,3 +278,20 @@ def test_ip_address_header_not_set():
     with h.push_config(config, **{'ip_address_header': 'X_FORWARDED_FOR'}):
         assert_equal(utils.ip_address(req),
                      '1.2.3.4')
+
+
+def test_empty_cursor():
+    """EmptyCursors conforms to specification of Ming's ODMCursor"""
+    cursor = utils.EmptyCursor()
+    assert_equal(cursor.count(), 0)
+    assert_equal(cursor.first(), None)
+    assert_equal(cursor.all(), [])
+    assert_equal(cursor.limit(10), cursor)
+    assert_equal(cursor.skip(10), cursor)
+    assert_equal(cursor.sort('name', 1), cursor)
+    assert_equal(cursor.hint('index'), cursor)
+    assert_equal(cursor.extensions, [])
+    assert_equal(cursor.options(arg1='val1', arg2='val2'), cursor)
+    assert_raises(ValueError, cursor.one)
+    assert_raises(StopIteration, cursor.next)
+    assert_raises(StopIteration, cursor._next_impl)


[14/37] allura git commit: [#7823] ticket:724 s/emai/email/

Posted by je...@apache.org.
[#7823] ticket:724 s/emai/email/


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

Branch: refs/heads/ib/4542
Commit: f155a687eff70872b852b6dd0c21c52ebb7658ab
Parents: 17d4276
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 12 10:28:05 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Fri Feb 13 20:48:35 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/f155a687/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index cdd71dc..3dbad4a 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -142,7 +142,7 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             if user is not None:
                 g.statsUpdater.newCommit(new, repo.app_config.project, user)
             actor = user or TransientActor(
-                    activity_name=new.committed.name or new.committed.emai)
+                    activity_name=new.committed.name or new.committed.email)
             g.director.create_activity(actor, 'committed', new,
                                        related_nodes=[repo.app_config.project],
                                        tags=['commit', repo.tool.lower()])


[36/37] allura git commit: [#4542] ticket:728 Don't enforce limit if multiple branches pushed at once

Posted by je...@apache.org.
[#4542] ticket:728 Don't enforce limit if multiple branches pushed at once


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

Branch: refs/heads/ib/4542
Commit: a60ccc945e61bd0ac91ddd46e0c9abd44f2368bd
Parents: 3fc560e
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 16:26:31 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:42 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py |  7 +++++--
 Allura/allura/webhooks.py           | 18 ++++++++++++++----
 2 files changed, 19 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/a60ccc94/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 38ea295..1dc17ba 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -166,12 +166,15 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
                 commits_by_tags[t].append(commit)
 
         from allura.webhooks import RepoPushWebhookSender
+        params = []
         for b, commits in commits_by_branches.iteritems():
             ref = u'refs/heads/{}'.format(b)
-            RepoPushWebhookSender().send(commit_ids=commits, ref=ref)
+            params.append(dict(commit_ids=commits, ref=ref))
         for t, commits in commits_by_tags.iteritems():
             ref = u'refs/tags/{}'.format(t)
-            RepoPushWebhookSender().send(commit_ids=commits, ref=ref)
+            params.append(dict(commit_ids=commits, ref=ref))
+        if params:
+            RepoPushWebhookSender().send(params)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event('repo_refreshed', len(commit_ids), all_commits, new_clone)

http://git-wip-us.apache.org/repos/asf/allura/blob/a60ccc94/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index d2302d8..f727b63 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -284,18 +284,28 @@ class WebhookSender(object):
         """Return a dict with webhook payload"""
         raise NotImplementedError('get_payload')
 
-    def send(self, **kw):
-        """Post a task that will send webhook 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:
-            payload = self.get_payload(**kw)
+            payloads = [self.get_payload(**params)
+                        for params in params_or_list]
             for webhook in webhooks:
                 if webhook.enforce_limit():
                     webhook.update_limit()
-                    send_webhook.post(webhook._id, payload)
+                    for payload in payloads:
+                        send_webhook.post(webhook._id, payload)
                 else:
                     log.warn('Webhook fires too often: %s. Skipping', webhook)
 


[02/37] allura git commit: [#5726] Refactored post_to_feed related functional tests.

Posted by je...@apache.org.
[#5726] Refactored post_to_feed related functional tests.


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

Branch: refs/heads/ib/4542
Commit: c9577b91192571e5267fe60f35299ac418b612d2
Parents: ef5610b
Author: Heith Seewald <hs...@slashdotmedia.com>
Authored: Mon Feb 2 12:47:22 2015 -0500
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Mon Feb 2 18:24:52 2015 +0000

----------------------------------------------------------------------
 Allura/allura/templates/widgets/edit_post.html  |  2 +-
 .../templates/widgets/new_topic_post.html       |  2 +-
 .../forgediscussion/controllers/root.py         |  1 +
 .../tests/functional/test_forum.py              | 85 ++++++++------------
 4 files changed, 37 insertions(+), 53 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/c9577b91/Allura/allura/templates/widgets/edit_post.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/widgets/edit_post.html b/Allura/allura/templates/widgets/edit_post.html
index a42803e..b3d814c 100644
--- a/Allura/allura/templates/widgets/edit_post.html
+++ b/Allura/allura/templates/widgets/edit_post.html
@@ -18,7 +18,7 @@
 -#}
 {% import 'allura:templates/jinja_master/lib.html' as lib with context %}
 <div>
-  <form method="post" action="{{action}}"
+  <form id="edit_{{ action[-4:] }}" method="post" action="{{action}}"
         enctype="multipart/form-data">
     {% if show_subject %}
     <input name="{{ widget.context_for(widget.fields.subject)['rendered_name'] }}" style="width:97%"

http://git-wip-us.apache.org/repos/asf/allura/blob/c9577b91/Allura/allura/templates/widgets/new_topic_post.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/widgets/new_topic_post.html b/Allura/allura/templates/widgets/new_topic_post.html
index 1c03b94..c7f572a 100644
--- a/Allura/allura/templates/widgets/new_topic_post.html
+++ b/Allura/allura/templates/widgets/new_topic_post.html
@@ -17,7 +17,7 @@
        under the License.
 -#}
 {% import 'allura:templates/jinja_master/lib.html' as lib with context %}
-<form method="post" action="{{action}}">
+<form id="create_new_topic" method="post" action="{{action}}">
     {% if show_subject %}
     <div class="grid-19">&nbsp;</div>
     <div class="grid-19">

http://git-wip-us.apache.org/repos/asf/allura/blob/c9577b91/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 9174fba..7548cd0 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -57,6 +57,7 @@ class RootController(BaseController, DispatchIndex, FeedController):
     class W(object):
         forum_subscription_form = FW.ForumSubscriptionForm()
         new_topic = DW.NewTopicPost(submit_text='Post')
+
         announcements_table = FW.AnnouncementsTable()
         add_forum = AddForumShort()
         search_results = SearchResults()

http://git-wip-us.apache.org/repos/asf/allura/blob/c9577b91/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
index 3f45b46..732c5c5 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_forum.py
@@ -40,7 +40,6 @@ log = logging.getLogger(__name__)
 
 
 class TestForumEmail(TestController):
-
     def setUp(self):
         TestController.setUp(self)
         c.user = M.User.by_username('test-admin')
@@ -127,7 +126,6 @@ class TestForumEmail(TestController):
 
 
 class TestForumAsync(TestController):
-
     def setUp(self):
         TestController.setUp(self)
         self.app.get('/discussion/')
@@ -269,7 +267,6 @@ class TestForumAsync(TestController):
 
 
 class TestForum(TestController):
-
     def setUp(self):
         TestController.setUp(self)
         self.app.get('/discussion/')
@@ -290,6 +287,33 @@ class TestForum(TestController):
         r = self.app.get('/admin/discussion/forums')
         assert 'childforum' in r
 
+    @staticmethod
+    def fill_thread_reply(r):
+        form = r.forms['edit_post']
+        for field in form.fields.values():
+            field = field[0]
+            if field.id is None:
+                continue
+            if 'text' in field.id:
+                form[field.name] = 'Test_Reply'
+        return form
+
+    @staticmethod
+    def fill_new_topic_form(r):
+        form = r.forms['create_new_topic']
+        for field in form.fields.values():
+            field = field[0]
+            if field.id is None:
+                continue
+            if 'subject' in field.id:
+                form[field.name] = 'Test_Subject'
+            if 'forum' in field.id:
+                form[field.name] = 'testforum'
+            if 'text' in field.id:
+                form[field.name] = 'Test_Description'
+        return form
+
+
     def test_unicode_name(self):
         r = self.app.get('/admin/discussion/forums')
         r.forms[1]['add_forum.shortname'] = u'téstforum'.encode('utf-8')
@@ -559,8 +583,7 @@ class TestForum(TestController):
                     'value') and field['value'] or ''
         params[f.find('textarea')['name']] = 'Post text'
         params[f.find('select')['name']] = 'testforum'
-        params[f.find('input', {'style': 'width: 90%'})
-               ['name']] = 'Post subject'
+        params[f.find('input', {'style': 'width: 90%'})['name']] = 'Post subject'
         thread = self.app.post(
             '/discussion/save_new_topic', params=params).follow()
         assert M.Notification.query.find(
@@ -636,65 +659,26 @@ class TestForum(TestController):
     def test_post_to_feed(self):
         # Create a new topic
         r = self.app.get('/discussion/create_topic/')
-        f = r.html.find(
-            'form', {'action': '/p/test/discussion/save_new_topic'})
-        params = dict()
-        inputs = f.findAll('input')
-        for field in inputs:
-            if field.has_key('name'):
-                params[field['name']] = field.has_key(
-                    'value') and field['value'] or ''
-        params[f.find('textarea')['name']] = 'XYZ'
-        params[f.find('select')['name']] = 'testforum'
-        params[f.find('input', {'style': 'width: 90%'})['name']] = 'AAAA'
-        thread = self.app.post('/discussion/save_new_topic', params=params).follow()
+        form = self.fill_new_topic_form(r)
+        thread = form.submit().follow()
         url = thread.request.url
 
         # Check that the newly created topic is the most recent in the rss feed
         f = self.app.get('/discussion/feed.rss').body
         f = feedparser.parse(f)
         newest_entry = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
-        assert newest_entry == 'XYZ'
+        assert newest_entry == 'Test_Description'
 
         # Reply to the newly created thread.
         thread = self.app.get(url)
-        t = thread.html.find(
-            'div', {'class': 'row reply_post_form'}).find('form')
-        rep_url = t.get('action')
-        params = dict()
-        inputs = t.findAll('input')
-        for field in inputs:
-            if field.has_key('name'):
-                params[field['name']] = field.has_key(
-                    'value') and field['value'] or ''
-        params[t.find('textarea')['name']] = 'bbb'
-        self.app.post(str(rep_url), params=params)
+        form = self.fill_thread_reply(thread)
+        form.submit()
 
         # Check that reply matches the newest in the rss feed
         f = self.app.get('/discussion/feed.rss').body
         f = feedparser.parse(f)
         newest_reply = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
-        assert newest_reply == 'bbb'
-
-    def test_post_to_feed(self):
-        r = self.app.get('/discussion/create_topic/')
-        f = r.html.find(
-            'form', {'action': '/p/test/discussion/save_new_topic'})
-        params = dict()
-        inputs = f.findAll('input')
-        for field in inputs:
-            if field.has_key('name'):
-                params[field['name']] = field.has_key(
-                    'value') and field['value'] or ''
-        params[f.find('textarea')['name']] = 'XYZ'
-        params[f.find('select')['name']] = 'testforum'
-        params[f.find('input', {'style': 'width: 90%'})['name']] = 'AAAA'
-        self.app.post('/discussion/save_new_topic', params=params)
-
-        f = self.app.get('/discussion/feed.rss').body
-        f = feedparser.parse(f)
-        newest_entry = f['entries'][0]['summary_detail']['value'].split("</p>")[0].split("<p>")[-1]
-        assert newest_entry == 'XYZ'
+        assert newest_reply == 'Test_Reply'
 
     def test_thread_sticky(self):
         r = self.app.get('/discussion/create_topic/')
@@ -884,7 +868,6 @@ class TestForum(TestController):
 
 
 class TestForumStats(TestController):
-
     def test_stats(self):
         self.app.get('/discussion/stats', status=200)
 


[04/37] allura git commit: [#6248] ticket:718 Fixed the truncation of long list items in comments

Posted by je...@apache.org.
[#6248] ticket:718 Fixed the truncation of long list items in comments


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

Branch: refs/heads/ib/4542
Commit: 1daebd1524139042b57cd185cb06bba8f258c731
Parents: 78b0707
Author: Aleksey 'LXj' Alekseyev <go...@gmail.com>
Authored: Wed Feb 4 22:37:06 2015 +0200
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Fri Feb 6 09:04:23 2015 +0000

----------------------------------------------------------------------
 Allura/allura/nf/allura/css/site_style.css | 3 +++
 1 file changed, 3 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1daebd15/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 4c62314..9f8f1b3 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -2529,6 +2529,9 @@ div.attachment_thumb .file_type span {
   left: 27px;
 }
 
+#comment .grid-14 { overflow: visible; }
+#comment .display_post { margin-right: 5px; }
+
 #comment ul {
   list-style: none;
   margin: 0 20px 20px 0;


[27/37] allura git commit: [#4542] ticket:726 Fix tests

Posted by je...@apache.org.
[#4542] ticket:726 Fix tests


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

Branch: refs/heads/ib/4542
Commit: 1ec65829bff616b2b31dcf86739374b3fac4cc7f
Parents: 10cbeea
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Feb 11 15:54:51 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:40 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_webhooks.py | 145 +++++++++++-------------------
 1 file changed, 53 insertions(+), 92 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1ec65829/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 5362da7..3d51f5c 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -18,7 +18,6 @@ from tg import config
 from allura import model as M
 from allura.lib import helpers as h
 from allura.webhooks import (
-    MingOneOf,
     WebhookValidator,
     WebhookController,
     send_webhook,
@@ -58,28 +57,12 @@ class TestWebhookBase(object):
 
 class TestValidators(TestWebhookBase):
 
-    def test_ming_one_of(self):
-        ids = [ac._id for ac in M.AppConfig.query.find().all()[:2]]
-        v = MingOneOf(cls=M.AppConfig, ids=ids, not_empty=True)
-        with assert_raises(Invalid) as cm:
-            v.to_python(None)
-        assert_equal(cm.exception.msg, u'Please enter a value')
-        with assert_raises(Invalid) as cm:
-            v.to_python('invalid id')
-        assert_equal(cm.exception.msg,
-            u'Object must be one of: %s, not invalid id' % ids)
-        assert_equal(v.to_python(ids[0]), M.AppConfig.query.get(_id=ids[0]))
-        assert_equal(v.to_python(ids[1]), M.AppConfig.query.get(_id=ids[1]))
-        assert_equal(v.to_python(unicode(ids[0])),
-                     M.AppConfig.query.get(_id=ids[0]))
-        assert_equal(v.to_python(unicode(ids[1])),
-                     M.AppConfig.query.get(_id=ids[1]))
-
+    @with_git2
     def test_webhook_validator(self):
         sender = Mock(type='repo-push')
-        ids = [ac._id for ac in M.AppConfig.query.find().all()[:3]]
-        ids, invalid_id = ids[:2], ids[2]
-        v = WebhookValidator(sender=sender, ac_ids=ids, not_empty=True)
+        app = self.git
+        invalid_app = self.project.app_instance('src2')
+        v = WebhookValidator(sender=sender, app=app, not_empty=True)
         with assert_raises(Invalid) as cm:
             v.to_python(None)
         assert_equal(cm.exception.msg, u'Please enter a value')
@@ -88,21 +71,23 @@ class TestValidators(TestWebhookBase):
         assert_equal(cm.exception.msg, u'Invalid webhook')
 
         wh = M.Webhook(type='invalid type',
-                       app_config_id=invalid_id,
-                       hook_url='http://httpbin.org/post',
+                       app_config_id=invalid_app.config._id,
+                       hook_url='http://hooks.slack.com',
                        secret='secret')
         session(wh).flush(wh)
+        # invalid type
         with assert_raises(Invalid) as cm:
             v.to_python(wh._id)
         assert_equal(cm.exception.msg, u'Invalid webhook')
 
         wh.type = 'repo-push'
         session(wh).flush(wh)
+        # invalild app
         with assert_raises(Invalid) as cm:
             v.to_python(wh._id)
         assert_equal(cm.exception.msg, u'Invalid webhook')
 
-        wh.app_config_id = ids[0]
+        wh.app_config_id = app.config._id
         session(wh).flush(wh)
         assert_equal(v.to_python(wh._id), wh)
         assert_equal(v.to_python(unicode(wh._id)), wh)
@@ -118,8 +103,7 @@ class TestWebhookController(TestController):
             p.start()
         self.project = M.Project.query.get(shortname=test_project_with_repo)
         self.git = self.project.app_instance('src')
-        self.git2 = self.project.app_instance('src2')
-        self.url = str(self.project.url() + 'admin/webhooks')
+        self.url = str(self.git.admin_url + 'webhooks')
 
     def tearDown(self):
         super(TestWebhookController, self).tearDown()
@@ -127,7 +111,6 @@ class TestWebhookController(TestController):
             p.stop()
 
     @with_git
-    @with_git2
     def setup_with_tools(self):
         pass
 
@@ -139,8 +122,9 @@ class TestWebhookController(TestController):
             autospec=True)
         return [gen_secret]
 
-    def create_webhook(self, data):
-        r = self.app.post(self.url + '/repo-push/create', data)
+    def create_webhook(self, data, url=None):
+        url = url or self.url
+        r = self.app.post(url + '/repo-push/create', data)
         wf = json.loads(self.webflash(r))
         assert_equal(wf['status'], 'ok')
         assert_equal(wf['message'], 'Created successfully')
@@ -151,8 +135,7 @@ class TestWebhookController(TestController):
         if field == '_the_form':
             error = form.findPrevious('div', attrs={'class': 'error'})
         else:
-            widget = 'select' if field == 'app' else 'input'
-            error = form.find(widget, attrs={'name': field})
+            error = form.find('input', attrs={'name': field})
             error = error.findNext('div', attrs={'class': 'error'})
         if error:
             assert_in(h.escape(msg), error.getText())
@@ -169,7 +152,7 @@ class TestWebhookController(TestController):
                          status=302)
         assert_equal(r.location,
             'http://localhost/auth/'
-            '?return_to=%2Fadobe%2Fadobe-1%2Fadmin%2Fwebhooks%2Frepo-push%2F')
+            '?return_to=%2Fadobe%2Fadobe-1%2Fadmin%2Fsrc%2Fwebhooks%2Frepo-push%2F')
 
     def test_invalid_hook_type(self):
         self.app.get(self.url + '/invalid-hook-type/', status=404)
@@ -180,12 +163,11 @@ class TestWebhookController(TestController):
         assert_in('<h1>repo-push</h1>', r)
         assert_not_in('http://httpbin.org/post', r)
         data = {'url': u'http://httpbin.org/post',
-                'app': unicode(self.git.config._id),
                 'secret': ''}
         msg = 'add webhook repo-push {} {}'.format(
             data['url'], self.git.config.url())
         with td.audits(msg):
-            r = self.create_webhook(data).follow().follow(status=200)
+            r = self.create_webhook(data).follow()
         assert_in('http://httpbin.org/post', r)
 
         hooks = M.Webhook.query.find().all()
@@ -207,9 +189,8 @@ class TestWebhookController(TestController):
         limit = json.dumps({'git': 1})
         with h.push_config(config, **{'webhook.repo_push.max_hooks': limit}):
             data = {'url': u'http://httpbin.org/post',
-                    'app': unicode(self.git.config._id),
                     'secret': ''}
-            r = self.create_webhook(data).follow().follow(status=200)
+            r = self.create_webhook(data).follow()
             assert_equal(M.Webhook.query.find().count(), 1)
 
             r = self.app.post(self.url + '/repo-push/create', data)
@@ -217,7 +198,7 @@ class TestWebhookController(TestController):
             assert_equal(wf['status'], 'error')
             assert_equal(
                 wf['message'],
-                'You have exceeded the maximum number of projects '
+                'You have exceeded the maximum number of webhooks '
                 'you are allowed to create for this project/app')
             assert_equal(M.Webhook.query.find().count(), 1)
 
@@ -226,43 +207,33 @@ class TestWebhookController(TestController):
         r = self.app.post(
             self.url + '/repo-push/create', {}, status=404)
 
-        data = {'url': '', 'app': '', 'secret': ''}
+        data = {'url': '', 'secret': ''}
         r = self.app.post(self.url + '/repo-push/create', data)
         self.find_error(r, 'url', 'Please enter a value')
-        self.find_error(r, 'app', 'Please enter a value')
 
-        data = {'url': 'qwer', 'app': '123', 'secret': 'qwe'}
+        data = {'url': 'qwer', 'secret': 'qwe'}
         r = self.app.post(self.url + '/repo-push/create', data)
         self.find_error(r, 'url',
             'You must provide a full domain name (like qwer.com)')
-        self.find_error(r, 'app', 'Object must be one of: ')
-        self.find_error(r, 'app', '%s' % self.git.config._id)
-        self.find_error(r, 'app', '%s' % self.git2.config._id)
 
     def test_edit(self):
         data1 = {'url': u'http://httpbin.org/post',
-                 'app': unicode(self.git.config._id),
                  'secret': u'secret'}
         data2 = {'url': u'http://example.com/hook',
-                 'app': unicode(self.git2.config._id),
                  'secret': u'secret2'}
-        self.create_webhook(data1).follow().follow(status=200)
-        self.create_webhook(data2).follow().follow(status=200)
+        self.create_webhook(data1).follow()
+        self.create_webhook(data2).follow()
         assert_equal(M.Webhook.query.find().count(), 2)
         wh1 = M.Webhook.query.get(hook_url=data1['url'])
         r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
         form = r.forms[0]
         assert_equal(form['url'].value, data1['url'])
-        assert_equal(form['app'].value, data1['app'])
         assert_equal(form['secret'].value, data1['secret'])
         assert_equal(form['webhook'].value, unicode(wh1._id))
         form['url'] = 'http://host.org/hook'
-        form['app'] = unicode(self.git2.config._id)
         form['secret'] = 'new secret'
-        msg = 'edit webhook repo-push\n{} => {}\n{} => {}\n{}'.format(
-            data1['url'], form['url'].value,
-            self.git.config.url(), self.git2.config.url(),
-            'secret changed')
+        msg = 'edit webhook repo-push\n{} => {}\n{}'.format(
+            data1['url'], form['url'].value, 'secret changed')
         with td.audits(msg):
             r = form.submit()
         wf = json.loads(self.webflash(r))
@@ -271,7 +242,7 @@ class TestWebhookController(TestController):
         assert_equal(M.Webhook.query.find().count(), 2)
         wh1 = M.Webhook.query.get(_id=wh1._id)
         assert_equal(wh1.hook_url, 'http://host.org/hook')
-        assert_equal(wh1.app_config_id, self.git2.config._id)
+        assert_equal(wh1.app_config_id, self.git.config._id)
         assert_equal(wh1.secret, 'new secret')
         assert_equal(wh1.type, 'repo-push')
 
@@ -279,10 +250,9 @@ class TestWebhookController(TestController):
         r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
         form = r.forms[0]
         form['url'] = data2['url']
-        form['app'] = data2['app']
         r = form.submit()
         self.find_error(r, '_the_form',
-            u'"repo-push" webhook already exists for Git2 http://example.com/hook',
+            u'"repo-push" webhook already exists for Git http://example.com/hook',
             form_type='edit')
 
     def test_edit_validation(self):
@@ -295,9 +265,8 @@ class TestWebhookController(TestController):
         self.app.get(self.url + '/repo-push/%s' % invalid._id, status=404)
 
         data = {'url': u'http://httpbin.org/post',
-                'app': unicode(self.git.config._id),
                 'secret': u'secret'}
-        self.create_webhook(data).follow().follow(status=200)
+        self.create_webhook(data).follow()
         wh = M.Webhook.query.get(hook_url=data['url'], type='repo-push')
 
         # invalid id in hidden field, just in case
@@ -307,25 +276,19 @@ class TestWebhookController(TestController):
         self.app.post(self.url + '/repo-push/edit', data, status=404)
 
         # empty values
-        data = {'url': '', 'app': '', 'secret': '', 'webhook': str(wh._id)}
+        data = {'url': '', 'secret': '', 'webhook': str(wh._id)}
         r = self.app.post(self.url + '/repo-push/edit', data)
         self.find_error(r, 'url', 'Please enter a value', 'edit')
-        self.find_error(r, 'app', 'Please enter a value', 'edit')
 
-        data = {'url': 'qwe', 'app': '123', 'secret': 'qwe',
-                'webhook': str(wh._id)}
+        data = {'url': 'qwe', 'secret': 'qwe', 'webhook': str(wh._id)}
         r = self.app.post(self.url + '/repo-push/edit', data)
         self.find_error(r, 'url',
             'You must provide a full domain name (like qwe.com)', 'edit')
-        self.find_error(r, 'app', 'Object must be one of:', 'edit')
-        self.find_error(r, 'app', '%s' % self.git.config._id, 'edit')
-        self.find_error(r, 'app', '%s' % self.git2.config._id, 'edit')
 
     def test_delete(self):
         data = {'url': u'http://httpbin.org/post',
-                'app': unicode(self.git.config._id),
                 'secret': u'secret'}
-        self.create_webhook(data).follow().follow(status=200)
+        self.create_webhook(data).follow()
         assert_equal(M.Webhook.query.find().count(), 1)
         wh = M.Webhook.query.get(hook_url=data['url'])
         data = {'webhook': unicode(wh._id)}
@@ -352,15 +315,19 @@ class TestWebhookController(TestController):
         self.app.post(self.url + '/repo-push/delete', data, status=404)
         assert_equal(M.Webhook.query.find().count(), 1)
 
+    @with_git2
     def test_list_webhooks(self):
+        git2 = self.project.app_instance('src2')
+        url2 = str(git2.admin_url + 'webhooks')
         data1 = {'url': u'http://httpbin.org/post',
-                 'app': unicode(self.git.config._id),
                  'secret': 'secret'}
         data2 = {'url': u'http://another-host.org/',
-                 'app': unicode(self.git2.config._id),
                  'secret': 'secret2'}
-        self.create_webhook(data1).follow().follow(status=200)
-        self.create_webhook(data2).follow().follow(status=200)
+        data3 = {'url': u'http://another-app.org/',
+                 'secret': 'secret3'}
+        self.create_webhook(data1).follow()
+        self.create_webhook(data2).follow()
+        self.create_webhook(data3, url=url2).follow()
         wh1 = M.Webhook.query.get(hook_url=data1['url'])
         wh2 = M.Webhook.query.get(hook_url=data2['url'])
 
@@ -370,22 +337,23 @@ class TestWebhookController(TestController):
         assert_equal(len(rows), 2)
         rows = sorted([self._format_row(row) for row in rows])
         expected_rows = sorted([
-            [{'href': self.url + '/repo-push/' + str(wh1._id),
-              'text': wh1.hook_url},
-             {'href': self.git.url,
-              'text': self.git.config.options.mount_label},
+            [{'text': wh1.hook_url},
              {'text': wh1.secret},
+             {'href': self.url + '/repo-push/' + str(wh1._id),
+              'text': u'Edit'},
              {'href': self.url + '/repo-push/delete',
               'data-id': str(wh1._id)}],
-            [{'href': self.url + '/repo-push/' + str(wh2._id),
-              'text': wh2.hook_url},
-             {'href': self.git2.url,
-              'text': self.git2.config.options.mount_label},
+            [{'text': wh2.hook_url},
              {'text': wh2.secret},
+             {'href': self.url + '/repo-push/' + str(wh2._id),
+              'text': u'Edit'},
              {'href': self.url + '/repo-push/delete',
               'data-id': str(wh2._id)}],
         ])
         assert_equal(rows, expected_rows)
+        # make sure webhooks for another app is not visible
+        assert_not_in(u'http://another-app.org/', r)
+        assert_not_in(u'secret3', r)
 
     def _format_row(self, row):
         def link(td):
@@ -397,7 +365,7 @@ class TestWebhookController(TestController):
             a = td.find('a')
             return {'href': a.get('href'), 'data-id': a.get('data-id')}
         tds = row.findAll('td')
-        return [link(tds[0]), link(tds[1]), text(tds[2]), delete_btn(tds[3])]
+        return [text(tds[0]), text(tds[1]), link(tds[2]), delete_btn(tds[3])]
 
 
 class TestSendWebhookHelper(TestWebhookBase):
@@ -568,30 +536,23 @@ class TestRepoPushWebhookSender(TestWebhookBase):
 
         sender = RepoPushWebhookSender()
         # default
-        assert_equal(sender.enforce_limit(self.git.config), True)
+        assert_equal(sender.enforce_limit(self.git), True)
         add_webhooks('one', 3)
-        assert_equal(sender.enforce_limit(self.git.config), False)
+        assert_equal(sender.enforce_limit(self.git), False)
 
         # config
         limit = json.dumps({'git': 5})
         with h.push_config(config, **{'webhook.repo_push.max_hooks': limit}):
-            assert_equal(sender.enforce_limit(self.git.config), True)
+            assert_equal(sender.enforce_limit(self.git), True)
             add_webhooks('two', 3)
-            assert_equal(sender.enforce_limit(self.git.config), False)
+            assert_equal(sender.enforce_limit(self.git), False)
 
 
 class TestModels(TestWebhookBase):
 
-    def test_webhook_find(self):
-        p = M.Project.query.get(shortname='test')
-        assert_equal(M.Webhook.find('smth', p), [])
-        assert_equal(M.Webhook.find('repo-push', p), [])
-        assert_equal(M.Webhook.find('smth', self.project), [])
-        assert_equal(M.Webhook.find('repo-push', self.project), [self.wh])
-
     def test_webhook_url(self):
         assert_equal(self.wh.url(),
-            '/adobe/adobe-1/admin/webhooks/repo-push/{}'.format(self.wh._id))
+            '/adobe/adobe-1/admin/src/webhooks/repo-push/{}'.format(self.wh._id))
 
     def test_webhook_enforce_limit(self):
         self.wh.last_sent = None


[16/37] allura git commit: [#4542] ticket:714 Basic webhooks framework & repo-push hook

Posted by je...@apache.org.
[#4542] ticket:714 Basic webhooks framework & repo-push hook


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

Branch: refs/heads/ib/4542
Commit: 0eb35a94ff74dc7d456f21a0bfa334763955916b
Parents: 0d6a443
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Jan 28 10:15:57 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:16:47 2015 +0000

----------------------------------------------------------------------
 Allura/allura/ext/admin/admin_main.py           |  29 ++++
 .../ext/admin/templates/webhooks_list.html      |  48 ++++++
 Allura/allura/lib/utils.py                      |   8 +
 Allura/allura/model/__init__.py                 |   1 +
 Allura/allura/model/repo_refresh.py             |   2 +
 Allura/allura/model/webhook.py                  |  42 ++++++
 .../allura/templates/webhooks/create_form.html  |  90 +++++++++++
 Allura/allura/webhooks.py                       | 148 +++++++++++++++++++
 Allura/setup.py                                 |   3 +
 9 files changed, 371 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py
index 26b058f..52b80f9 100644
--- a/Allura/allura/ext/admin/admin_main.py
+++ b/Allura/allura/ext/admin/admin_main.py
@@ -30,6 +30,7 @@ from webob import exc
 from bson import ObjectId
 from ming.orm.ormsession import ThreadLocalORMSession
 from ming.odm import session
+from ming.utils import LazyProperty
 from allura.app import Application, DefaultAdminController, SitemapEntry
 from allura.lib import helpers as h
 from allura import version
@@ -147,6 +148,7 @@ class AdminApp(Application):
                     SitemapEntry('Categorization', admin_url + 'trove')
                 ]
         links.append(SitemapEntry('Tools', admin_url + 'tools'))
+        links.append(SitemapEntry('Webhooks', admin_url + 'webhooks'))
         if asbool(config.get('bulk_export_enabled', True)):
             links.append(SitemapEntry('Export', admin_url + 'export'))
         if c.project.is_root and has_access(c.project, 'admin')():
@@ -191,6 +193,32 @@ class AdminExtensionLookup(object):
         raise exc.HTTPNotFound, name
 
 
+class WebhooksLookup(BaseController):
+
+    @LazyProperty
+    def _webhooks(self):
+        webhooks = h.iter_entry_points('allura.webhooks')
+        webhooks = [ep.load() for ep in webhooks]
+        return webhooks
+
+    @with_trailing_slash
+    @expose('jinja:allura.ext.admin:templates/webhooks_list.html')
+    def index(self):
+        webhooks = self._webhooks
+        configured_hooks = {}
+        for hook in webhooks:
+            configured_hooks[hook.type] = M.Webhook.find(hook.type, c.project)
+        return {'webhooks': webhooks,
+                'configured_hooks': configured_hooks}
+
+    @expose()
+    def _lookup(self, name, *remainder):
+        for hook in self._webhooks:
+            if hook.type == name and hook.controller:
+                return hook.controller(hook), remainder
+        raise exc.HTTPNotFound, name
+
+
 class ProjectAdminController(BaseController):
     def _check_security(self):
         require_access(c.project, 'admin')
@@ -200,6 +228,7 @@ class ProjectAdminController(BaseController):
         self.groups = GroupsController()
         self.audit = AuditController()
         self.ext = AdminExtensionLookup()
+        self.webhooks = WebhooksLookup()
 
     @with_trailing_slash
     @expose('jinja:allura.ext.admin:templates/project_admin.html')

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/ext/admin/templates/webhooks_list.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/webhooks_list.html b/Allura/allura/ext/admin/templates/webhooks_list.html
new file mode 100644
index 0000000..9bb2476
--- /dev/null
+++ b/Allura/allura/ext/admin/templates/webhooks_list.html
@@ -0,0 +1,48 @@
+{#-
+       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.
+-#}
+{% extends g.theme.master %}
+
+{% block title %}{{c.project.name}} / Webhooks{% endblock %}
+{% block header %}Webhooks{% endblock %}
+
+{% block content %}
+  {% for hook in webhooks %}
+    <h1>{{ hook.type }}</h1>
+    <p><a href="{{ hook.type }}">Create</a></p>
+    {% if configured_hooks[hook.type] %}
+      <table>
+        {% for h in configured_hooks[hook.type] %}
+        <tr>
+          <td>
+            <a href="{{ h.url() }}">{{ h.hook_url }}</a>
+          </td>
+          <td>
+            <a href="{{ h.app_config.url() }}">{{ h.app_config.options.mount_label }}</a>
+          </td>
+          <td>
+            <a href="#" title="Delete">
+              <b data-icon="{{g.icons['delete'].char}}" class="ico {{g.icons['delete'].css}}" title="Delete"></b>
+            </a>
+          </td>
+        </tr>
+        {% endfor %}
+      </table>
+    {% endif %}
+  {% endfor %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/lib/utils.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index 7a56603..83950c6 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -32,6 +32,7 @@ import collections
 
 import tg
 import pylons
+import json
 import webob.multidict
 from formencode import Invalid
 from tg.decorators import before_validate
@@ -592,3 +593,10 @@ class EmptyCursor(ODMCursor):
 
     def sort(self, *args, **kw):
         return self
+
+
+class DateJSONEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, datetime.datetime):
+            return obj.strftime('%Y-%m-%dT%H:%M:%SZ')
+        return json.JSONEncoder.default(self, obj)

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/model/__init__.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/__init__.py b/Allura/allura/model/__init__.py
index a74bff6..784d1af 100644
--- a/Allura/allura/model/__init__.py
+++ b/Allura/allura/model/__init__.py
@@ -34,6 +34,7 @@ from .repository import MergeRequest, GitLikeTree
 from .stats import Stats
 from .oauth import OAuthToken, OAuthConsumerToken, OAuthRequestToken, OAuthAccessToken
 from .monq_model import MonQTask
+from .webhook import Webhook
 
 from .types import ACE, ACL, EVERYONE, ALL_PERMISSIONS, DENY_ALL, MarkdownCache
 from .session import main_doc_session, main_orm_session

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 3dbad4a..86953cc 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -146,6 +146,8 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             g.director.create_activity(actor, 'committed', new,
                                        related_nodes=[repo.app_config.project],
                                        tags=['commit', repo.tool.lower()])
+        from allura.webhooks import RepoPushWebhookSender
+        RepoPushWebhookSender().send(commit_ids=commit_ids)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event('repo_refreshed', len(commit_ids), all_commits, new_clone)

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/model/webhook.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
new file mode 100644
index 0000000..73b9540
--- /dev/null
+++ b/Allura/allura/model/webhook.py
@@ -0,0 +1,42 @@
+#       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 ming.odm import FieldProperty
+from allura.model import Artifact
+
+
+class Webhook(Artifact):
+
+    class __mongometa__:
+        name = 'webhook'
+        unique_indexes = [('app_config_id', 'type', 'hook_url')]
+
+    type = FieldProperty(str)
+    hook_url = FieldProperty(str)
+
+    def url(self):
+        return '{}{}/{}/{}'.format(
+            self.app_config.project.url(),
+            'admin/webhooks',
+            self.type,
+            self._id)
+
+    @classmethod
+    def find(cls, type, project):
+        ac_ids = [ac._id for ac in project.app_configs]
+        hooks = cls.query.find(dict(type=type, app_config_id={'$in': ac_ids}))
+        return hooks.all()

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/templates/webhooks/create_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/webhooks/create_form.html b/Allura/allura/templates/webhooks/create_form.html
new file mode 100644
index 0000000..a44bb5b
--- /dev/null
+++ b/Allura/allura/templates/webhooks/create_form.html
@@ -0,0 +1,90 @@
+{#-
+       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.
+-#}
+{% extends g.theme.master %}
+
+{% block title %}{{c.project.name}} / Create {{webhook.type}} webhook{% endblock %}
+
+{% block header %}Create {{webhook.type}} webhook{% endblock %}
+
+{% block extra_css %}
+  <style type="text/css">
+    form {
+      padding: 0 20px 20px 20px;
+    }
+    form label {
+      display: inline-block;
+      width: 30%;
+      vertical-align: top;
+    }
+    form > div {
+      margin-bottom: 10px;
+    }
+    form > div input {
+      width: 30%;
+      vertical-align: top;
+    }
+    form > div input[type="checkbox"] {
+      -moz-box-shadow: none;
+      -webkit-box-shadow: none;
+      -o-box-shadow: none;
+      box-shadow: none;
+      width: 1em;
+    }
+    form .error {
+      display: inline-block;
+      color: #f00;
+      background: none;
+      border: none;
+      margin: 0;
+      width: 30%;
+    }
+  </style>
+{% endblock %}
+
+{%- macro error(field_name) %}
+  {% if c.form_errors[field_name] %}
+  <div class="error">{{c.form_errors[field_name]}}</div>
+  {% endif %}
+{%- endmacro %}
+
+{% block content %}
+<form action="create" method="post" enctype="multipart/form-data">
+  <div>
+    <label for="url">url</label>
+    <input name="url" value="{{ c.form_values['url'] }}">
+    {{ error('url') }}
+  </div>
+  <div>
+    <label for="app">app</label>
+    <select name="app">
+      {% for ac in form.triggered_by %}
+        <option value="{{ac._id}}"{% if h.really_unicode(ac._id) == c.form_values['app'] %} selected{% endif %}>
+          {{ ac.options.mount_label }}
+        </option>
+      {% endfor %}
+    </select>
+    {{ error('app') }}
+  </div>
+
+  {% block additional_fields %}{% endblock %}
+
+  <input type="submit" value="Create">
+  {{lib.csrf_token()}}
+</form>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
new file mode 100644
index 0000000..c5dc37e
--- /dev/null
+++ b/Allura/allura/webhooks.py
@@ -0,0 +1,148 @@
+#       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 requests
+from bson import ObjectId
+from tg import expose, validate, redirect
+from tg.decorators import with_trailing_slash
+from pylons import tmpl_context as c
+from formencode import validators as fev, schema
+from ming.odm import session
+
+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 WebhookCreateForm(schema.Schema):
+    def __init__(self, hook):
+        super(WebhookCreateForm, self).__init__()
+        self.triggered_by = [ac for ac in c.project.app_configs
+                             if ac.tool_name.lower() in hook.triggered_by]
+        self.add_field('app', fev.OneOf(
+            [unicode(ac._id) for ac in self.triggered_by]))
+
+    url = fev.URL(not_empty=True)
+
+
+class WebhookControllerMeta(type):
+    def __call__(cls, hook, *args, **kw):
+        """Decorate the `create` post handler with a validator that references
+        the appropriate webhook sender for this controller.
+        """
+        if hasattr(cls, 'create'):
+            cls.create = validate(
+                cls.create_form(hook),
+                error_handler=cls.index.__func__,
+            )(cls.create)
+        return type.__call__(cls, hook, *args, **kw)
+
+
+class WebhookController(BaseController):
+    __metaclass__ = WebhookControllerMeta
+    create_form = WebhookCreateForm
+
+    def __init__(self, hook):
+        super(WebhookController, self).__init__()
+        self.webhook = hook
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/webhooks/create_form.html')
+    def index(self, **kw):
+        return {'webhook': self.webhook,
+                'form': self.create_form(self.webhook)}
+
+    @expose()
+    @require_post()
+    def create(self, url, app):
+        # TODO: catch DuplicateKeyError
+        wh = M.Webhook(
+            hook_url=url,
+            app_config_id=ObjectId(app),
+            type=self.webhook.type)
+        session(wh).flush(wh)
+        redirect(c.project.url() + 'admin/webhooks/')
+
+
+@task()
+def send_webhook(webhook_id, payload):
+    webhook = M.Webhook.query.get(_id=webhook_id)
+    url = webhook.hook_url
+    headers = {'content-type': 'application/json'}
+    json_payload = json.dumps(payload, cls=DateJSONEncoder)
+    # TODO: catch
+    # TODO: configurable timeout
+    r = requests.post(url, data=json_payload, headers=headers, timeout=30)
+    if r.status_code >= 200 and r.status_code <= 300:
+        log.info('Webhook successfully sent: %s %s %s',
+                 webhook.type, webhook.hook_url, webhook.app_config.url())
+    else:
+        # TODO: retry
+        # TODO: configurable retries
+        log.error('Webhook send error: %s %s %s %s %s',
+                  webhook.type, webhook.hook_url,
+                  webhook.app_config.url(),
+                  r.status_code, r.reason)
+
+
+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
+
+    def get_payload(self, **kw):
+        """Return a dict with webhook payload"""
+        raise NotImplementedError('get_payload')
+
+    def send(self, **kw):
+        """Post a task that will send webhook payload"""
+        webhooks = M.Webhook.query.find(dict(
+            app_config_id=c.app.config._id,
+            type=self.type,
+        )).all()
+        if webhooks:
+            payload = self.get_payload(**kw)
+            for webhook in webhooks:
+                send_webhook.post(webhook._id, payload)
+
+
+class RepoPushWebhookSender(WebhookSender):
+    type = 'repo-push'
+    triggered_by = ['git', 'hg', 'svn']
+
+    def get_payload(self, commit_ids, **kw):
+        app = kw.get('app') or c.app
+        payload = {
+            'url': h.absurl(app.url),
+            'count': len(commit_ids),
+            'revisions': [app.repo.commit(ci).info for ci in commit_ids],
+        }
+        return payload

http://git-wip-us.apache.org/repos/asf/allura/blob/0eb35a94/Allura/setup.py
----------------------------------------------------------------------
diff --git a/Allura/setup.py b/Allura/setup.py
index 8eabf60..e44c0c2 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -135,6 +135,9 @@ setup(
     tools = allura.ext.user_profile.user_main:ToolsSection
     social = allura.ext.user_profile.user_main:SocialSection
 
+    [allura.webhooks]
+    repo-push = allura.webhooks:RepoPushWebhookSender
+
     [paste.paster_command]
     taskd = allura.command.taskd:TaskdCommand
     taskd_cleanup = allura.command.taskd_cleanup:TaskdCleanupCommand


[03/37] allura git commit: [#7795] ticket:717 Fix test failure due to mim access from two threads

Posted by je...@apache.org.
[#7795] ticket:717 Fix test failure due to mim access from two threads


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

Branch: refs/heads/ib/4542
Commit: 78b0707396d45deea91de1eeff4dfe5fe82c4b84
Parents: c9577b9
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Feb 3 12:09:31 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Tue Feb 3 12:09:31 2015 +0000

----------------------------------------------------------------------
 ForgeWiki/forgewiki/tests/test_models.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/78b07073/ForgeWiki/forgewiki/tests/test_models.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/test_models.py b/ForgeWiki/forgewiki/tests/test_models.py
index ac07669..5c10e50 100644
--- a/ForgeWiki/forgewiki/tests/test_models.py
+++ b/ForgeWiki/forgewiki/tests/test_models.py
@@ -30,18 +30,24 @@ class TestPageSnapshots(TestController):
         # details https://sourceforge.net/p/allura/tickets/7647/
         import time
         import random
-        from threading import Thread
+        from threading import Thread, Lock
 
         page = Page.upsert('test-page')
         page.commit()
 
+        lock = Lock()
         def run(n):
             setup_global_objects()
             for i in range(10):
                 page = Page.query.get(title='test-page')
                 page.text = 'Test Page %s.%s' % (n, i)
                 time.sleep(random.random())
-                page.commit()
+                # tests use mim (mongo-in-memory), which isn't thread-safe
+                lock.acquire()
+                try:
+                    page.commit()
+                finally:
+                    lock.release()
 
         t1 = Thread(target=lambda: run(1))
         t2 = Thread(target=lambda: run(2))


[23/37] allura git commit: [#4542] ticket:715 Add thresholds

Posted by je...@apache.org.
[#4542] ticket:715 Add thresholds


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

Branch: refs/heads/ib/4542
Commit: e7c60a345d82844f9d529d9171991b74b60f3515
Parents: 5c82af91
Author: Igor Bondarenko <je...@gmail.com>
Authored: Mon Feb 2 12:53:03 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:38 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_webhooks.py | 42 +++++++++++++++++++++++++++++++
 Allura/allura/webhooks.py            | 31 +++++++++++++++++------
 Allura/development.ini               |  6 +++++
 3 files changed, 72 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/e7c60a34/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 99e5f91..791128e 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -202,6 +202,25 @@ class TestWebhookController(TestController):
             '"repo-push" webhook already exists for Git http://httpbin.org/post')
         assert_equal(M.Webhook.query.find().count(), 1)
 
+    def test_create_limit_reached(self):
+        assert_equal(M.Webhook.query.find().count(), 0)
+        limit = json.dumps({'git': 1})
+        with h.push_config(config, **{'webhook.repo_push.max_hooks': limit}):
+            data = {'url': u'http://httpbin.org/post',
+                    'app': unicode(self.git.config._id),
+                    'secret': ''}
+            r = self.create_webhook(data).follow().follow(status=200)
+            assert_equal(M.Webhook.query.find().count(), 1)
+
+            r = self.app.post(self.url + '/repo-push/create', data)
+            wf = json.loads(self.webflash(r))
+            assert_equal(wf['status'], 'error')
+            assert_equal(
+                wf['message'],
+                'You have exceeded the maximum number of projects '
+                'you are allowed to create for this project/app')
+            assert_equal(M.Webhook.query.find().count(), 1)
+
     def test_create_validation(self):
         assert_equal(M.Webhook.query.find().count(), 0)
         r = self.app.post(
@@ -468,6 +487,29 @@ class TestRepoPushWebhookSender(TestWebhookBase):
         }
         assert_equal(result, expected_result)
 
+    def test_enforce_limit(self):
+        def add_webhooks(suffix, n):
+            for i in range(n):
+                webhook = M.Webhook(
+                    type='repo-push',
+                    app_config_id=self.git.config._id,
+                    hook_url='http://httpbin.org/{}/{}'.format(suffix, i),
+                    secret='secret')
+                session(webhook).flush(webhook)
+
+        sender = RepoPushWebhookSender()
+        # default
+        assert_equal(sender.enforce_limit(self.git.config), True)
+        add_webhooks('one', 3)
+        assert_equal(sender.enforce_limit(self.git.config), False)
+
+        # config
+        limit = json.dumps({'git': 5})
+        with h.push_config(config, **{'webhook.repo_push.max_hooks': limit}):
+            assert_equal(sender.enforce_limit(self.git.config), True)
+            add_webhooks('two', 3)
+            assert_equal(sender.enforce_limit(self.git.config), False)
+
 
 class TestModels(TestWebhookBase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/e7c60a34/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index e13086d..7df8a42 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -22,7 +22,7 @@ import hashlib
 
 import requests
 from bson import ObjectId
-from tg import expose, validate, redirect, flash
+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 formencode import validators as fev, schema, Invalid
@@ -125,7 +125,7 @@ class WebhookController(BaseController):
 
     def __init__(self, sender):
         super(WebhookController, self).__init__()
-        self.sender = sender
+        self.sender = sender()
 
     def gen_secret(self):
         return h.cryptographic_nonce(20)
@@ -169,11 +169,15 @@ class WebhookController(BaseController):
     @expose()
     @require_post()
     def create(self, url, app, secret):
-        wh = M.Webhook(type=self.sender.type)
-        self.update_webhook(wh, url, app, secret)
-        M.AuditLog.log('add webhook %s %s %s',
-                       wh.type, wh.hook_url, wh.app_config.url())
-        flash('Created successfully', 'ok')
+        if self.sender.enforce_limit(app):
+            wh = M.Webhook(type=self.sender.type)
+            self.update_webhook(wh, url, app, secret)
+            M.AuditLog.log('add webhook %s %s %s',
+                           wh.type, wh.hook_url, wh.app_config.url())
+            flash('Created successfully', 'ok')
+        else:
+            flash('You have exceeded the maximum number of projects '
+                  'you are allowed to create for this project/app', 'error')
         redirect(c.project.url() + 'admin/webhooks/')
 
     @expose()
@@ -278,6 +282,19 @@ class WebhookSender(object):
                 else:
                     log.warn('Webhook fires too often: %s. Skipping', webhook)
 
+    def enforce_limit(self, app_config):
+        '''
+        Checks if limit of webhooks created for given project/app is reached.
+        Returns False if limit is reached, True otherwise.
+        '''
+        _type = self.type.replace('-', '_')
+        limits = json.loads(config.get('webhook.%s.max_hooks' % _type, '{}'))
+        count = M.Webhook.query.find(dict(
+            app_config_id=app_config._id,
+            type=self.type,
+        )).count()
+        return count < limits.get(app_config.tool_name.lower(), 3)
+
 
 class RepoPushWebhookSender(WebhookSender):
     type = 'repo-push'

http://git-wip-us.apache.org/repos/asf/allura/blob/e7c60a34/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 39a9efa..f08ffb7 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -135,6 +135,12 @@ user_prefs.maximum_claimed_emails = 20
 # e.g. for repo-push webhook:
 # webhook.repo_push.limit = 10
 
+# Limit max number of hooks that can be created for given project/app
+# Option name format: same as above.
+# Value format: json dict, where keys are app names (as appears in
+# `WebhookSender.triggered_by`) and values are actual limits (default=3), e.g.:
+# webhook.repo_push.max_hooks = {"git": 3, "hg": 3, "svn": 3}
+
 # Additional fields for admin project/user search
 # Note: whitespace after comma is important!
 # search.project.additional_search_fields = private, url, title


[10/37] allura git commit: Fix unicode handling in changelog script

Posted by je...@apache.org.
Fix unicode handling in changelog script


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

Branch: refs/heads/ib/4542
Commit: 1f019a1e1713d47f9d6872bf2691bba1b7e9769c
Parents: 6face2d
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 12:42:37 2015 +0200
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Fri Feb 13 12:48:03 2015 +0200

----------------------------------------------------------------------
 scripts/changelog.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1f019a1e/scripts/changelog.py
----------------------------------------------------------------------
diff --git a/scripts/changelog.py b/scripts/changelog.py
index d6ed617..8f49e94 100755
--- a/scripts/changelog.py
+++ b/scripts/changelog.py
@@ -66,7 +66,7 @@ def print_changelog(version, summaries):
         'date': datetime.utcnow().strftime('%B %Y'),
     })
     for ticket in sorted(summaries.keys()):
-        print " * [#{0}] {1}".format(ticket, summaries[ticket])
+        print " * [#{0}] {1}".format(ticket, summaries[ticket].encode('utf-8'))
 
 if __name__ == '__main__':
     main()


[31/37] allura git commit: [#4542] ticket:728 Add ref to webhook payload

Posted by je...@apache.org.
[#4542] ticket:728 Add ref to webhook payload


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

Branch: refs/heads/ib/4542
Commit: 3fc560e566c1dbd850514e368b109b2c621dabb0
Parents: a22f6f6
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 15:19:49 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:41 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py | 26 +++++++++++++++++++++++++-
 Allura/allura/webhooks.py           | 22 +++++++++++++++++++++-
 2 files changed, 46 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/3fc560e5/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 86953cc..38ea295 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -134,6 +134,10 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
                 log.info('Compute last commit info %d: %s', (i + 1), ci._id)
 
     if not all_commits and not new_clone:
+        commits_by_branches = {}
+        commits_by_tags = {}
+        current_branches = []
+        current_tags = []
         for commit in commit_ids:
             new = repo.commit(commit)
             user = User.by_email_address(new.committed.email)
@@ -146,8 +150,28 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             g.director.create_activity(actor, 'committed', new,
                                        related_nodes=[repo.app_config.project],
                                        tags=['commit', repo.tool.lower()])
+
+            branches, tags = repo.symbolics_for_commit(new)
+            if branches:
+                current_branches = branches
+            if tags:
+                current_tags = tags
+            for b in current_branches:
+                if b not in commits_by_branches.keys():
+                    commits_by_branches[b] = []
+                commits_by_branches[b].append(commit)
+            for t in current_tags:
+                if t not in commits_by_tags.keys():
+                    commits_by_tags[t] = []
+                commits_by_tags[t].append(commit)
+
         from allura.webhooks import RepoPushWebhookSender
-        RepoPushWebhookSender().send(commit_ids=commit_ids)
+        for b, commits in commits_by_branches.iteritems():
+            ref = u'refs/heads/{}'.format(b)
+            RepoPushWebhookSender().send(commit_ids=commits, ref=ref)
+        for t, commits in commits_by_tags.iteritems():
+            ref = u'refs/tags/{}'.format(t)
+            RepoPushWebhookSender().send(commit_ids=commits, ref=ref)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event('repo_refreshed', len(commit_ids), all_commits, new_clone)

http://git-wip-us.apache.org/repos/asf/allura/blob/3fc560e5/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 233c3d5..d2302d8 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -36,7 +36,6 @@ from paste.deploy.converters import asint, aslist
 
 from allura.controllers import BaseController
 from allura.lib import helpers as h
-from allura.lib import validators as av
 from allura.lib.decorators import require_post, task
 from allura.lib.utils import DateJSONEncoder
 from allura import model as M
@@ -318,16 +317,37 @@ 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 parents[-1]
+        return u''
+
+    def _after(self, repo, commit_ids):
+        if len(commit_ids) > 0:
+            return commit_ids[0]
+        return u''
+
     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]
+        before = self._before(app.repo, commit_ids)
+        after = self._after(app.repo, 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


[09/37] allura git commit: [#7831] Fixed an issue that prevented users from logging out

Posted by je...@apache.org.
[#7831] Fixed an issue that prevented users from logging out


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

Branch: refs/heads/ib/4542
Commit: 6bc50feb600b2ba642c84354442e3028dce12dc8
Parents: 70df2e8
Author: Heith Seewald <hs...@slashdotmedia.com>
Authored: Tue Feb 10 15:40:54 2015 -0500
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Feb 11 17:10:21 2015 +0000

----------------------------------------------------------------------
 .../allura/controllers/basetest_project_root.py | 33 +++++++++++---------
 Allura/allura/lib/plugin.py                     |  1 +
 Allura/allura/tests/functional/test_auth.py     | 17 ++++++++--
 3 files changed, 35 insertions(+), 16 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/6bc50feb/Allura/allura/controllers/basetest_project_root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/basetest_project_root.py b/Allura/allura/controllers/basetest_project_root.py
index a2df7a2..459a62f 100644
--- a/Allura/allura/controllers/basetest_project_root.py
+++ b/Allura/allura/controllers/basetest_project_root.py
@@ -41,7 +41,6 @@ log = logging.getLogger(__name__)
 
 
 class BasetestProjectRootController(WsgiDispatchController, ProjectController):
-
     '''Root controller for testing -- it behaves just like a
     ProjectController for test/ except that all tools are mounted,
     on-demand, at the mount point that is the same as their entry point
@@ -119,23 +118,32 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
         return app.root, remainder
 
     def __call__(self, environ, start_response):
+        """ Called from a turbo gears 'app' instance.
+
+
+        :param environ: Extra environment variables.
+        Example: self.app.get('/auth/', extra_environ={'disable_auth_magic': "True"})
+        """
         c.app = None
         c.project = M.Project.query.get(
             shortname='test', neighborhood_id=self.p_nbhd._id)
-        auth = plugin.AuthenticationProvider.get(request)
-        user = auth.by_username(environ.get('username', 'test-admin'))
-        if not user:
-            user = M.User.anonymous()
-        environ['beaker.session']['username'] = user.username
-        # save and persist, so that a creation time is set
-        environ['beaker.session'].save()
-        environ['beaker.session'].persist()
-        c.user = auth.authenticate_request()
+        if 'disable_auth_magic' in environ:
+            auth = plugin.AuthenticationProvider.get(request)
+            c.user = auth.authenticate_request()
+        else:
+            auth = plugin.AuthenticationProvider.get(request)
+            user = auth.by_username(environ.get('username', 'test-admin'))
+            if not user:
+                user = M.User.anonymous()
+            environ['beaker.session']['username'] = user.username
+            # save and persist, so that a creation time is set
+            environ['beaker.session'].save()
+            environ['beaker.session'].persist()
+            c.user = auth.authenticate_request()
         return WsgiDispatchController.__call__(self, environ, start_response)
 
 
 class DispatchTest(object):
-
     @expose()
     def _lookup(self, *args):
         if args:
@@ -145,7 +153,6 @@ class DispatchTest(object):
 
 
 class NamedController(object):
-
     def __init__(self, name):
         self.name = name
 
@@ -159,7 +166,6 @@ class NamedController(object):
 
 
 class SecurityTests(object):
-
     @expose()
     def _lookup(self, name, *args):
         name = unquote(name)
@@ -169,7 +175,6 @@ class SecurityTests(object):
 
 
 class SecurityTest(object):
-
     def __init__(self):
         from forgewiki import model as WM
         c.app = c.project.app_instance('wiki')

http://git-wip-us.apache.org/repos/asf/allura/blob/6bc50feb/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index f3f90a8..a901ba9 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -176,6 +176,7 @@ class AuthenticationProvider(object):
 
     def logout(self):
         self.session.invalidate()
+        self.session.save()
         response.delete_cookie('allura-loggedin')
 
     def validate_password(self, user, password):

http://git-wip-us.apache.org/repos/asf/allura/blob/6bc50feb/Allura/allura/tests/functional/test_auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index ce0e4e4..bfd3c72 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -74,6 +74,21 @@ class TestAuth(TestController):
             username='test-usera', password='foo'))
         assert 'Invalid login' in str(r), r.showbrowser()
 
+    def test_logout(self):
+        environ = {'disable_auth_magic': "True"}
+        r = self.app.get('/auth/', extra_environ=environ)
+        f = r.forms[0]
+        f['username'] = 'test-user'
+        f['password'] = 'foo'
+        r = f.submit().follow(extra_environ=environ)
+        logged_in_session = r.session['_id']
+        assert r.html.nav('a')[-1].string == "Log Out"
+
+        r = self.app.get('/auth/logout', extra_environ=environ).follow(extra_environ=environ)
+        logged_out_session = r.session['_id']
+        assert logged_in_session is not logged_out_session
+        assert r.html.nav('a')[-1].string == 'Log In'
+
     def test_track_login(self):
         user = M.User.by_username('test-user')
         assert_equal(user.last_access['login_date'], None)
@@ -297,7 +312,6 @@ class TestAuth(TestController):
                           params=dict(a=email_address),
                           extra_environ=dict(username='test-user'))
 
-
         user1 = M.User.query.get(username='test-user-1')
         user1.claim_address(email_address)
         email1 = M.EmailAddress.find(dict(email=email_address, claimed_by_user_id=user1._id)).first()
@@ -1503,7 +1517,6 @@ class TestDisableAccount(TestController):
 
 
 class TestPasswordExpire(TestController):
-
     def login(self, username='test-user', pwd='foo', query_string=''):
         r = self.app.get('/auth/' + query_string, extra_environ={'username': '*anonymous'})
         f = r.forms[0]


[08/37] allura git commit: [#7831] minor code improvements, including:

Posted by je...@apache.org.
[#7831] minor code improvements, including:

* disable_auth_magic=False should do what you expect
* set extra_environ on the app instead of on each request in the test
* assert_equal for more helpful failure output


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

Branch: refs/heads/ib/4542
Commit: 6face2dfee01a32bd1559f772a5b09c2d09c142b
Parents: 6bc50fe
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Wed Feb 11 17:01:23 2015 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Feb 11 17:10:21 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/basetest_project_root.py |  8 ++++----
 Allura/allura/tests/functional/test_auth.py        | 12 ++++++------
 2 files changed, 10 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/6face2df/Allura/allura/controllers/basetest_project_root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/basetest_project_root.py b/Allura/allura/controllers/basetest_project_root.py
index 459a62f..f655437 100644
--- a/Allura/allura/controllers/basetest_project_root.py
+++ b/Allura/allura/controllers/basetest_project_root.py
@@ -25,6 +25,7 @@ from pylons import tmpl_context as c
 from pylons import request
 from webob import exc
 from tg import expose
+from paste.deploy.converters import asbool
 
 from allura.lib.base import WsgiDispatchController
 from allura.lib.security import require, require_authenticated, require_access, has_access
@@ -118,7 +119,7 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
         return app.root, remainder
 
     def __call__(self, environ, start_response):
-        """ Called from a turbo gears 'app' instance.
+        """ Called from a WebTest 'app' instance.
 
 
         :param environ: Extra environment variables.
@@ -127,11 +128,10 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
         c.app = None
         c.project = M.Project.query.get(
             shortname='test', neighborhood_id=self.p_nbhd._id)
-        if 'disable_auth_magic' in environ:
-            auth = plugin.AuthenticationProvider.get(request)
+        auth = plugin.AuthenticationProvider.get(request)
+        if asbool(environ.get('disable_auth_magic')):
             c.user = auth.authenticate_request()
         else:
-            auth = plugin.AuthenticationProvider.get(request)
             user = auth.by_username(environ.get('username', 'test-admin'))
             if not user:
                 user = M.User.anonymous()

http://git-wip-us.apache.org/repos/asf/allura/blob/6face2df/Allura/allura/tests/functional/test_auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index bfd3c72..b71ffcd 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -75,19 +75,19 @@ class TestAuth(TestController):
         assert 'Invalid login' in str(r), r.showbrowser()
 
     def test_logout(self):
-        environ = {'disable_auth_magic': "True"}
-        r = self.app.get('/auth/', extra_environ=environ)
+        self.app.extra_environ = {'disable_auth_magic': 'True'}
+        r = self.app.get('/auth/')
         f = r.forms[0]
         f['username'] = 'test-user'
         f['password'] = 'foo'
-        r = f.submit().follow(extra_environ=environ)
+        r = f.submit().follow()
         logged_in_session = r.session['_id']
-        assert r.html.nav('a')[-1].string == "Log Out"
+        assert_equal(r.html.nav('a')[-1].string, "Log Out")
 
-        r = self.app.get('/auth/logout', extra_environ=environ).follow(extra_environ=environ)
+        r = self.app.get('/auth/logout').follow()
         logged_out_session = r.session['_id']
         assert logged_in_session is not logged_out_session
-        assert r.html.nav('a')[-1].string == 'Log In'
+        assert_equal(r.html.nav('a')[-1].string, 'Log In')
 
     def test_track_login(self):
         user = M.User.by_username('test-user')


[25/37] allura git commit: [#4542] ticket:723 Tweak error message

Posted by je...@apache.org.
[#4542] ticket:723 Tweak error message


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

Branch: refs/heads/ib/4542
Commit: 1b1dcfdf92874c5506f74dd5b7b1d3831694bd51
Parents: 072654c
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Feb 10 11:17:11 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:39 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_webhooks.py | 19 +++++++++++++------
 Allura/allura/webhooks.py            |  7 ++++---
 2 files changed, 17 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1b1dcfdf/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index ab82cb8..1879beb 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -430,9 +430,14 @@ class TestSendWebhookHelper(TestWebhookBase):
         assert_equal(
             self.h.log_msg('OK'),
             'OK: repo-push http://httpbin.org/post /adobe/adobe-1/src/')
+        response = Mock(
+            status_code=500,
+            text='that is why',
+            headers={'Content-Type': 'application/json'})
         assert_equal(
-            self.h.log_msg('Error', response=Mock(status_code=500, reason='that is why')),
-            'Error: repo-push http://httpbin.org/post /adobe/adobe-1/src/ 500 that is why')
+            self.h.log_msg('Error', response=response),
+            "Error: repo-push http://httpbin.org/post /adobe/adobe-1/src/ 500 "
+            "that is why {'Content-Type': 'application/json'}")
 
     @patch('allura.webhooks.SendWebhookHelper', autospec=True)
     def test_send_webhook_task(self, swh):
@@ -473,11 +478,12 @@ class TestSendWebhookHelper(TestWebhookBase):
             call('Retrying webhook in %s seconds', 240)])
         assert_equal(log.error.call_count, 4)
         log.error.assert_called_with(
-            'Webhook send error: %s %s %s %s %s' % (
+            'Webhook send error: %s %s %s %s %s %s' % (
                 self.wh.type, self.wh.hook_url,
                 self.wh.app_config.url(),
                 requests.post.return_value.status_code,
-                requests.post.return_value.reason))
+                requests.post.return_value.text,
+                requests.post.return_value.headers))
 
     @patch('allura.webhooks.time', autospec=True)
     @patch('allura.webhooks.requests', autospec=True)
@@ -491,11 +497,12 @@ class TestSendWebhookHelper(TestWebhookBase):
             log.info.assert_called_once_with('Retrying webhook in: %s', [])
             assert_equal(log.error.call_count, 1)
             log.error.assert_called_with(
-                'Webhook send error: %s %s %s %s %s' % (
+                'Webhook send error: %s %s %s %s %s %s' % (
                     self.wh.type, self.wh.hook_url,
                     self.wh.app_config.url(),
                     requests.post.return_value.status_code,
-                    requests.post.return_value.reason))
+                    requests.post.return_value.text,
+                    requests.post.return_value.headers))
 
 
 class TestRepoPushWebhookSender(TestWebhookBase):

http://git-wip-us.apache.org/repos/asf/allura/blob/1b1dcfdf/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index ebfb3d0..0f9b54d 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -256,11 +256,12 @@ class SendWebhookHelper(object):
             self.webhook.type,
             self.webhook.hook_url,
             self.webhook.app_config.url())
-        if response:
-            message = '{} {} {}'.format(
+        if response is not None:
+            message = '{} {} {} {}'.format(
                 message,
                 response.status_code,
-                response.reason)
+                response.text,
+                response.headers)
         return message
 
     def send(self):


[24/37] allura git commit: [#4542] ticket:715 Better error handling & retries

Posted by je...@apache.org.
[#4542] ticket:715 Better error handling & retries


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

Branch: refs/heads/ib/4542
Commit: 072654cfe94503a669d004a8094ad271572c80be
Parents: e7c60a3
Author: Igor Bondarenko <je...@gmail.com>
Authored: Mon Feb 2 15:45:42 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:39 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_webhooks.py | 106 +++++++++++++++++++++++-------
 Allura/allura/webhooks.py            |  99 +++++++++++++++++++++-------
 Allura/development.ini               |   5 ++
 3 files changed, 163 insertions(+), 47 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/072654cf/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 791128e..ab82cb8 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -3,7 +3,7 @@ import hmac
 import hashlib
 import datetime as dt
 
-from mock import Mock, patch
+from mock import Mock, patch, call
 from nose.tools import (
     assert_raises,
     assert_equal,
@@ -17,13 +17,13 @@ from tg import config
 
 from allura import model as M
 from allura.lib import helpers as h
-from allura.lib.utils import DateJSONEncoder
 from allura.webhooks import (
     MingOneOf,
     WebhookValidator,
     WebhookController,
     send_webhook,
     RepoPushWebhookSender,
+    SendWebhookHelper,
 )
 from allura.tests import decorators as td
 from alluratest.controller import setup_basic_test, TestController
@@ -400,45 +400,103 @@ class TestWebhookController(TestController):
         return [link(tds[0]), link(tds[1]), text(tds[2]), delete_btn(tds[3])]
 
 
-class TestTasks(TestWebhookBase):
+class TestSendWebhookHelper(TestWebhookBase):
 
-    @patch('allura.webhooks.requests', autospec=True)
-    @patch('allura.webhooks.log', autospec=True)
-    def test_send_webhook(self, log, requests):
-        requests.post.return_value = Mock(status_code=200)
-        payload = {'some': ['data']}
-        json_payload = json.dumps(payload, cls=DateJSONEncoder)
-        send_webhook(self.wh._id, payload)
+    def setUp(self, *args, **kw):
+        super(TestSendWebhookHelper, self).setUp(*args, **kw)
+        self.payload = {'some': ['data', 23]}
+        self.h = SendWebhookHelper(self.wh, self.payload)
+
+    def test_timeout(self):
+        assert_equal(self.h.timeout, 30)
+        with h.push_config(config, **{'webhook.timeout': 10}):
+            assert_equal(self.h.timeout, 10)
+
+    def test_retries(self):
+        assert_equal(self.h.retries, [60, 120, 240])
+        with h.push_config(config, **{'webhook.retry': '1 2 3 4 5 6'}):
+            assert_equal(self.h.retries, [1, 2, 3, 4, 5, 6])
+
+    def test_sign(self):
+        json_payload = json.dumps(self.payload)
         signature = hmac.new(
             self.wh.secret.encode('utf-8'),
             json_payload.encode('utf-8'),
             hashlib.sha1)
         signature = 'sha1=' + signature.hexdigest()
+        assert_equal(self.h.sign(json_payload), signature)
+
+    def test_log_msg(self):
+        assert_equal(
+            self.h.log_msg('OK'),
+            'OK: repo-push http://httpbin.org/post /adobe/adobe-1/src/')
+        assert_equal(
+            self.h.log_msg('Error', response=Mock(status_code=500, reason='that is why')),
+            'Error: repo-push http://httpbin.org/post /adobe/adobe-1/src/ 500 that is why')
+
+    @patch('allura.webhooks.SendWebhookHelper', autospec=True)
+    def test_send_webhook_task(self, swh):
+        send_webhook(self.wh._id, self.payload)
+        swh.assert_called_once_with(self.wh, self.payload)
+
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send(self, log, requests):
+        requests.post.return_value = Mock(status_code=200)
+        self.h.sign = Mock(return_value='sha1=abc')
+        self.h.send()
         headers = {'content-type': 'application/json',
                    'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
-                   'X-Allura-Signature': signature}
+                   'X-Allura-Signature': 'sha1=abc'}
         requests.post.assert_called_once_with(
             self.wh.hook_url,
-            data=json_payload,
+            data=json.dumps(self.payload),
             headers=headers,
             timeout=30)
         log.info.assert_called_once_with(
-            'Webhook successfully sent: %s %s %s',
-            self.wh.type, self.wh.hook_url, self.wh.app_config.url())
+            'Webhook successfully sent: %s %s %s' % (
+                self.wh.type, self.wh.hook_url, self.wh.app_config.url()))
 
+    @patch('allura.webhooks.time', autospec=True)
     @patch('allura.webhooks.requests', autospec=True)
     @patch('allura.webhooks.log', autospec=True)
-    def test_send_webhook_error(self, log, requests):
+    def test_send_error_response_status(self, log, requests, time):
         requests.post.return_value = Mock(status_code=500)
-        send_webhook(self.wh._id, {})
-        assert_equal(requests.post.call_count, 1)
-        assert_equal(log.info.call_count, 0)
-        log.error.assert_called_once_with(
-            'Webhook send error: %s %s %s %s %s',
-            self.wh.type, self.wh.hook_url,
-            self.wh.app_config.url(),
-            requests.post.return_value.status_code,
-            requests.post.return_value.reason)
+        self.h.send()
+        assert_equal(requests.post.call_count, 4)  # initial call + 3 retries
+        assert_equal(time.sleep.call_args_list,
+                     [call(60), call(120), call(240)])
+        assert_equal(log.info.call_args_list, [
+            call('Retrying webhook in: %s', [60, 120, 240]),
+            call('Retrying webhook in %s seconds', 60),
+            call('Retrying webhook in %s seconds', 120),
+            call('Retrying webhook in %s seconds', 240)])
+        assert_equal(log.error.call_count, 4)
+        log.error.assert_called_with(
+            'Webhook send error: %s %s %s %s %s' % (
+                self.wh.type, self.wh.hook_url,
+                self.wh.app_config.url(),
+                requests.post.return_value.status_code,
+                requests.post.return_value.reason))
+
+    @patch('allura.webhooks.time', autospec=True)
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send_error_no_retries(self, log, requests, time):
+        requests.post.return_value = Mock(status_code=500)
+        with h.push_config(config, **{'webhook.retry': ''}):
+            self.h.send()
+            assert_equal(requests.post.call_count, 1)
+            assert_equal(time.call_count, 0)
+            log.info.assert_called_once_with('Retrying webhook in: %s', [])
+            assert_equal(log.error.call_count, 1)
+            log.error.assert_called_with(
+                'Webhook send error: %s %s %s %s %s' % (
+                    self.wh.type, self.wh.hook_url,
+                    self.wh.app_config.url(),
+                    requests.post.return_value.status_code,
+                    requests.post.return_value.reason))
+
 
 class TestRepoPushWebhookSender(TestWebhookBase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/072654cf/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 7df8a42..ebfb3d0 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -19,6 +19,9 @@ import logging
 import json
 import hmac
 import hashlib
+import time
+import socket
+import ssl
 
 import requests
 from bson import ObjectId
@@ -29,6 +32,7 @@ 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
@@ -224,32 +228,81 @@ class WebhookController(BaseController):
                 'form': form}
 
 
+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 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:
+            message = '{} {} {}'.format(
+                message,
+                response.status_code,
+                response.reason)
+        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)
-    url = webhook.hook_url
-    json_payload = json.dumps(payload, cls=DateJSONEncoder)
-    signature = hmac.new(
-        webhook.secret.encode('utf-8'),
-        json_payload.encode('utf-8'),
-        hashlib.sha1)
-    signature = 'sha1=' + signature.hexdigest()
-    headers = {'content-type': 'application/json',
-               'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
-               'X-Allura-Signature': signature}
-    # TODO: catch
-    # TODO: configurable timeout
-    r = requests.post(url, data=json_payload, headers=headers, timeout=30)
-    if r.status_code >= 200 and r.status_code < 300:
-        log.info('Webhook successfully sent: %s %s %s',
-                 webhook.type, webhook.hook_url, webhook.app_config.url())
-    else:
-        # TODO: retry
-        # TODO: configurable retries
-        log.error('Webhook send error: %s %s %s %s %s',
-                  webhook.type, webhook.hook_url,
-                  webhook.app_config.url(),
-                  r.status_code, r.reason)
+    SendWebhookHelper(webhook, payload).send()
 
 
 class WebhookSender(object):

http://git-wip-us.apache.org/repos/asf/allura/blob/072654cf/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index f08ffb7..68b6267 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -129,6 +129,11 @@ user_prefs_storage.ldap.fields.display_name = cn
 # Limit the number of emails a user can claim.
 user_prefs.maximum_claimed_emails = 20
 
+# webhook.timeout = 30  # seconds, default = 30
+
+# List of pauses between retries, if hook fails (in seconds)
+# webhook.retry = 60 120 240
+
 # Limit rate of webhook firing (in seconds, default = 30)
 # Option format: webhook.<hook type>.limit,
 # all '-' in hook type must be changed to '_'


[34/37] allura git commit: [#4542] ticket:728 Fix tests

Posted by je...@apache.org.
[#4542] ticket:728 Fix tests


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

Branch: refs/heads/ib/4542
Commit: a433fa9778b8aaea2efada1f8df23f20291f09cf
Parents: fea9a04
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 17:35:24 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:42 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_webhooks.py            | 20 +++++++++++---------
 .../forgegit/tests/model/test_repository.py     | 16 ++++++----------
 .../forgesvn/tests/model/test_repository.py     | 17 ++++++-----------
 3 files changed, 23 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/a433fa97/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 3d51f5c..629b322 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -3,7 +3,7 @@ import hmac
 import hashlib
 import datetime as dt
 
-from mock import Mock, patch, call
+from mock import Mock, MagicMock, patch, call
 from nose.tools import (
     assert_raises,
     assert_equal,
@@ -480,7 +480,7 @@ class TestRepoPushWebhookSender(TestWebhookBase):
         sender = RepoPushWebhookSender()
         sender.get_payload = Mock()
         with h.push_config(c, app=self.git):
-            sender.send(arg1=1, arg2=2)
+            sender.send(dict(arg1=1, arg2=2))
         send_webhook.post.assert_called_once_with(
             self.wh._id,
             sender.get_payload.return_value)
@@ -492,7 +492,7 @@ class TestRepoPushWebhookSender(TestWebhookBase):
         sender.get_payload = Mock()
         self.wh.enforce_limit = Mock(return_value=False)
         with h.push_config(c, app=self.git):
-            sender.send(arg1=1, arg2=2)
+            sender.send(dict(arg1=1, arg2=2))
         assert_equal(send_webhook.post.call_count, 0)
         log.warn.assert_called_once_with(
             'Webhook fires too often: %s. Skipping', self.wh)
@@ -503,19 +503,21 @@ class TestRepoPushWebhookSender(TestWebhookBase):
         session(self.wh).flush(self.wh)
         sender = RepoPushWebhookSender()
         with h.push_config(c, app=self.git):
-            sender.send(arg1=1, arg2=2)
+            sender.send(dict(arg1=1, arg2=2))
         assert_equal(send_webhook.post.call_count, 0)
 
     def test_get_payload(self):
         sender = RepoPushWebhookSender()
-        _ci = list(range(1, 4))
-        _se = [Mock(webhook_info=str(x)) for x in _ci]
-        with patch.object(self.git.repo, 'commit', autospec=True, side_effect=_se):
+        _ci = lambda x: MagicMock(webhook_info={'id': str(x)}, parent_ids=['0'])
+        with patch.object(self.git.repo, 'commit', new=_ci):
             with h.push_config(c, app=self.git):
-                result = sender.get_payload(commit_ids=_ci)
+                result = sender.get_payload(commit_ids=['1', '2', '3'], ref='ref')
         expected_result = {
             'size': 3,
-            'commits': ['1', '2', '3'],
+            'commits': [{'id': '1'}, {'id': '2'}, {'id': '3'}],
+            'ref': u'ref',
+            'after': u'1',
+            'before': u'0',
             'repository': {
                 'full_name': u'/adobe/adobe-1/src/',
                 'name': u'Git',

http://git-wip-us.apache.org/repos/asf/allura/blob/a433fa97/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index b67a7ae..231e479 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -31,6 +31,7 @@ from ming.base import Object
 from ming.orm import ThreadLocalORMSession, session
 from nose.tools import assert_equal
 from testfixtures import TempDirectory
+from datadiff.tools import assert_equals
 
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura.lib import helpers as h
@@ -546,9 +547,12 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
 
         sender = RepoPushWebhookSender()
         cids = list(self.repo.all_commit_ids())[:2]
-        payload = sender.get_payload(commit_ids=cids)
+        payload = sender.get_payload(commit_ids=cids, ref='refs/heads/zz')
         expected_payload = {
             'size': 2,
+            'ref': u'refs/heads/zz',
+            'after': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
+            'before': u'df30427c488aeab84b2352bdf88a3b19223f9d7a',
             'commits': [{
                 'id': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
                 'url': u'http://localhost/p/test/src-git/ci/5c47243c8e424136fd5cdd18cd94d34c66d1955c/',
@@ -586,15 +590,7 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
                 'url': u'http://localhost/p/test/src-git/',
             },
         }
-
-        def _diff(one, two):
-            from difflib import Differ
-            from pprint import pformat
-            one, two = pformat(one), pformat(two)
-            diff = Differ().compare(one.splitlines(), two.splitlines())
-            print '\n'.join(diff)
-
-        assert payload == expected_payload, _diff(expected_payload, payload)
+        assert_equals(payload, expected_payload)
 
 
 class TestGitImplementation(unittest.TestCase):

http://git-wip-us.apache.org/repos/asf/allura/blob/a433fa97/ForgeSVN/forgesvn/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 030931a..eb952e1 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -28,6 +28,7 @@ from collections import defaultdict
 from pylons import tmpl_context as c, app_globals as g
 import mock
 from nose.tools import assert_equal
+from datadiff.tools import assert_equals
 import tg
 import ming
 from ming.base import Object
@@ -584,8 +585,10 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
         payload = sender.get_payload(commit_ids=cids)
         expected_payload = {
             'size': 2,
+            'after': 'r6',
+            'before': 'r4',
             'commits': [{
-                'id': u'{}:6'.format(self.repo._id),
+                'id': u'r6',
                 'url': u'http://localhost/p/test/src/6/',
                 'timestamp': datetime(2013, 11, 8, 13, 38, 11, 152000),
                 'message': u'',
@@ -600,7 +603,7 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
                 'modified': [],
                 'copied': []
             }, {
-                'id': u'{}:5'.format(self.repo._id),
+                'id': u'r5',
                 'url': u'http://localhost/p/test/src/5/',
                 'timestamp': datetime(2010, 11, 18, 20, 14, 21, 515000),
                 'message': u'Copied a => b',
@@ -621,15 +624,7 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
                 'url': u'http://localhost/p/test/src/',
             },
         }
-
-        def _diff(one, two):
-            from difflib import Differ
-            from pprint import pformat
-            one, two = pformat(one), pformat(two)
-            diff = Differ().compare(one.splitlines(), two.splitlines())
-            print '\n'.join(diff)
-
-        assert payload == expected_payload, _diff(expected_payload, payload)
+        assert_equals(payload, expected_payload)
 
 
 class TestSVNRev(unittest.TestCase):


[26/37] allura git commit: [#4542] ticket:723 Change repo-push hook payload

Posted by je...@apache.org.
[#4542] ticket:723 Change repo-push hook payload


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

Branch: refs/heads/ib/4542
Commit: 13a8a607ac69ef602ed0ef9992af310a58f92051
Parents: 1b1dcfd
Author: Igor Bondarenko <je...@gmail.com>
Authored: Tue Feb 10 13:55:28 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:39 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/repository.py               | 35 +++++++-
 Allura/allura/tests/test_webhooks.py            | 12 ++-
 Allura/allura/webhooks.py                       | 11 ++-
 .../forgegit/tests/model/test_repository.py     | 88 ++++++++++++++++----
 .../forgesvn/tests/model/test_repository.py     | 73 ++++++++++++----
 5 files changed, 174 insertions(+), 45 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/13a8a607/Allura/allura/model/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 5650d35..0030aa5 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -939,14 +939,22 @@ class Commit(RepoObject, ActivityObject):
         self.repo = repo
 
     @LazyProperty
+    def authored_user(self):
+        return User.by_email_address(self.authored.email)
+
+    @LazyProperty
+    def committed_user(self):
+        return User.by_email_address(self.committed.email)
+
+    @LazyProperty
     def author_url(self):
-        u = User.by_email_address(self.authored.email)
+        u = self.authored_user
         if u:
             return u.url()
 
     @LazyProperty
     def committer_url(self):
-        u = User.by_email_address(self.committed.email)
+        u = self.committed_user
         if u:
             return u.url()
 
@@ -1222,6 +1230,29 @@ class Commit(RepoObject, ActivityObject):
             summary=self.summary
         )
 
+    @LazyProperty
+    def webhook_info(self):
+        return {
+            'id': self._id,
+            'url': h.absurl(self.url()),
+            'timestamp': self.authored.date,
+            'message': self.summary,
+            'author': {
+                'name': self.authored.name,
+                'email': self.authored.email,
+                'username': self.authored_user.username if self.authored_user else u'',
+            },
+            'committer': {
+                'name': self.committed.name,
+                'email': self.committed.email,
+                'username': self.committed_user.username if self.committed_user else u'',
+            },
+            'added': self.diffs.added,
+            'removed': self.diffs.removed,
+            'modified': self.diffs.changed,
+            'copied': self.diffs.copied,
+        }
+
 
 class Tree(RepoObject):
     # Ephemeral attrs

http://git-wip-us.apache.org/repos/asf/allura/blob/13a8a607/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 1879beb..5362da7 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -541,14 +541,18 @@ class TestRepoPushWebhookSender(TestWebhookBase):
     def test_get_payload(self):
         sender = RepoPushWebhookSender()
         _ci = list(range(1, 4))
-        _se = [Mock(info=str(x)) for x in _ci]
+        _se = [Mock(webhook_info=str(x)) for x in _ci]
         with patch.object(self.git.repo, 'commit', autospec=True, side_effect=_se):
             with h.push_config(c, app=self.git):
                 result = sender.get_payload(commit_ids=_ci)
         expected_result = {
-            'url': 'http://localhost/adobe/adobe-1/src/',
-            'count': 3,
-            'revisions': ['1', '2', '3'],
+            'size': 3,
+            'commits': ['1', '2', '3'],
+            'repository': {
+                'full_name': u'/adobe/adobe-1/src/',
+                'name': u'Git',
+                'url': u'http://localhost/adobe/adobe-1/src/',
+            },
         }
         assert_equal(result, expected_result)
 

http://git-wip-us.apache.org/repos/asf/allura/blob/13a8a607/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 0f9b54d..17d7ebf 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -356,9 +356,14 @@ class RepoPushWebhookSender(WebhookSender):
 
     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]
         payload = {
-            'url': h.absurl(app.url),
-            'count': len(commit_ids),
-            'revisions': [app.repo.commit(ci).info for ci in commit_ids],
+            'size': len(commits),
+            'commits': commits,
+            'repository': {
+                'name': app.config.options.mount_label,
+                'full_name': app.url,
+                'url': h.absurl(app.url),
+            },
         }
         return payload

http://git-wip-us.apache.org/repos/asf/allura/blob/13a8a607/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 5e86ab0..b67a7ae 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -113,6 +113,20 @@ class TestNewGit(unittest.TestCase):
                 '/p/test/src-git/ci/'
                 '1e146e67985dcd71c74de79613719bef7bddca4a/')
 
+        assert_equal(self.rev.authored_user, None)
+        assert_equal(self.rev.committed_user, None)
+        user = M.User.upsert('rick')
+        email = user.claim_address('rcopeland@geek.net')
+        email.confirmed = True
+        session(email).flush(email)
+        rev = self.repo.commit(self.rev._id)  # to update cached values of LazyProperty
+        assert_equal(rev.authored_user, user)
+        assert_equal(rev.committed_user, user)
+        assert_equal(
+            sorted(rev.webhook_info.keys()),
+            sorted(['id', 'url', 'timestamp', 'message', 'author',
+                    'committer', 'added', 'removed', 'modified', 'copied']))
+
 
 class TestGitRepo(unittest.TestCase, RepoImplTestBase):
 
@@ -521,28 +535,66 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
                 'https://user@foo.com/')
 
     def test_webhook_payload(self):
+        user = M.User.upsert('cory')
+        email = user.claim_address('cjohns@slashdotmedia.com')
+        email.confirmed = True
+        session(email).flush(email)
+        user = M.User.upsert('rick')
+        email = user.claim_address('rcopeland@geek.net')
+        email.confirmed = True
+        session(email).flush(email)
+
         sender = RepoPushWebhookSender()
         cids = list(self.repo.all_commit_ids())[:2]
         payload = sender.get_payload(commit_ids=cids)
         expected_payload = {
-            'url': 'http://localhost/p/test/src-git/',
-            'count': 2,
-            'revisions': [
-                {'author': u'Cory Johns',
-                 'author_email': u'cjohns@slashdotmedia.com',
-                 'author_url': None,
-                 'date': datetime.datetime(2013, 3, 28, 18, 54, 16),
-                 'id': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
-                 'shortlink': u'[5c4724]',
-                 'summary': u'Not repo root'},
-                {'author': u'Rick Copeland',
-                 'author_email': u'rcopeland@geek.net',
-                 'author_url': None,
-                 'date': datetime.datetime(2010, 10, 7, 18, 44, 11),
-                 'id': u'1e146e67985dcd71c74de79613719bef7bddca4a',
-                 'shortlink': u'[1e146e]',
-                 'summary': u'Change README'}]}
-        assert_equal(payload, expected_payload)
+            'size': 2,
+            'commits': [{
+                'id': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
+                'url': u'http://localhost/p/test/src-git/ci/5c47243c8e424136fd5cdd18cd94d34c66d1955c/',
+                'timestamp': datetime.datetime(2013, 3, 28, 18, 54, 16),
+                'message': u'Not repo root',
+                'author': {'name': u'Cory Johns',
+                           'email': u'cjohns@slashdotmedia.com',
+                           'username': 'cory'},
+                'committer': {'name': u'Cory Johns',
+                              'email': u'cjohns@slashdotmedia.com',
+                              'username': 'cory'},
+                'added': [u'bad'],
+                'removed': [],
+                'modified': [],
+                'copied': []
+            }, {
+                'id': u'1e146e67985dcd71c74de79613719bef7bddca4a',
+                'url': u'http://localhost/p/test/src-git/ci/1e146e67985dcd71c74de79613719bef7bddca4a/',
+                'timestamp': datetime.datetime(2010, 10, 7, 18, 44, 11),
+                'message': u'Change README',
+                'author': {'name': u'Rick Copeland',
+                           'email': u'rcopeland@geek.net',
+                           'username': 'rick'},
+                'committer': {'name': u'Rick Copeland',
+                              'email': u'rcopeland@geek.net',
+                              'username': 'rick'},
+                'added': [],
+                'removed': [],
+                'modified': [u'README'],
+                'copied': []
+            }],
+            'repository': {
+                'name': u'Git',
+                'full_name': u'/p/test/src-git/',
+                'url': u'http://localhost/p/test/src-git/',
+            },
+        }
+
+        def _diff(one, two):
+            from difflib import Differ
+            from pprint import pformat
+            one, two = pformat(one), pformat(two)
+            diff = Differ().compare(one.splitlines(), two.splitlines())
+            print '\n'.join(diff)
+
+        assert payload == expected_payload, _diff(expected_payload, payload)
 
 
 class TestGitImplementation(unittest.TestCase):

http://git-wip-us.apache.org/repos/asf/allura/blob/13a8a607/ForgeSVN/forgesvn/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 5267614..030931a 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -1,3 +1,4 @@
+# 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
@@ -98,6 +99,13 @@ class TestNewRepo(unittest.TestCase):
         assert self.rev.tree['a']['b']['c'].ls() == []
         self.assertRaises(KeyError, lambda: self.rev.tree['a']['b']['d'])
 
+        assert_equal(self.rev.authored_user, None)
+        assert_equal(self.rev.committed_user, None)
+        assert_equal(
+            sorted(self.rev.webhook_info.keys()),
+            sorted(['id', 'url', 'timestamp', 'message', 'author',
+                    'committer', 'added', 'removed', 'modified', 'copied']))
+
 
 class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
 
@@ -575,24 +583,53 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
         cids = list(self.repo.all_commit_ids())[:2]
         payload = sender.get_payload(commit_ids=cids)
         expected_payload = {
-            'url': 'http://localhost/p/test/src/',
-            'count': 2,
-            'revisions': [
-                {'author': u'coldmind',
-                 'author_email': u'',
-                 'author_url': None,
-                 'date': datetime(2013, 11, 8, 13, 38, 11, 152000),
-                 'id': u'{}:6'.format(self.repo._id),
-                 'shortlink': '[r6]',
-                 'summary': ''},
-                {'author': u'rick446',
-                 'author_email': u'',
-                 'author_url': None,
-                 'date': datetime(2010, 11, 18, 20, 14, 21, 515000),
-                 'id': u'{}:5'.format(self.repo._id),
-                 'shortlink': '[r5]',
-                 'summary': u'Copied a => b'}]}
-        assert_equal(payload, expected_payload)
+            'size': 2,
+            'commits': [{
+                'id': u'{}:6'.format(self.repo._id),
+                'url': u'http://localhost/p/test/src/6/',
+                'timestamp': datetime(2013, 11, 8, 13, 38, 11, 152000),
+                'message': u'',
+                'author': {'name': u'coldmind',
+                           'email': u'',
+                           'username': u''},
+                'committer': {'name': u'coldmind',
+                              'email': u'',
+                              'username': u''},
+                'added': [u'/ЗРЯЧИЙ_ТА_ПОБАЧИТЬ'],
+                'removed': [],
+                'modified': [],
+                'copied': []
+            }, {
+                'id': u'{}:5'.format(self.repo._id),
+                'url': u'http://localhost/p/test/src/5/',
+                'timestamp': datetime(2010, 11, 18, 20, 14, 21, 515000),
+                'message': u'Copied a => b',
+                'author': {'name': u'rick446',
+                           'email': u'',
+                           'username': u''},
+                'committer': {'name': u'rick446',
+                              'email': u'',
+                              'username': u''},
+                'added': [u'/b'],
+                'removed': [],
+                'modified': [],
+                'copied': []
+            }],
+            'repository': {
+                'name': u'SVN',
+                'full_name': u'/p/test/src/',
+                'url': u'http://localhost/p/test/src/',
+            },
+        }
+
+        def _diff(one, two):
+            from difflib import Differ
+            from pprint import pformat
+            one, two = pformat(one), pformat(two)
+            diff = Differ().compare(one.splitlines(), two.splitlines())
+            print '\n'.join(diff)
+
+        assert payload == expected_payload, _diff(expected_payload, payload)
 
 
 class TestSVNRev(unittest.TestCase):


[33/37] allura git commit: [#4542] ticket:728 Convert svn ids

Posted by je...@apache.org.
[#4542] ticket:728 Convert svn ids


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

Branch: refs/heads/ib/4542
Commit: fea9a04054614af5e46fffe51e7bfc7f4eaf69b4
Parents: 5e5a19d
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Feb 13 17:01:08 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:42 2015 +0000

----------------------------------------------------------------------
 Allura/allura/webhooks.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/fea9a040/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index f727b63..03a909d 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -334,17 +334,24 @@ class RepoPushWebhookSender(WebhookSender):
             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 parents[-1]
+                return self._convert_id(parents[-1])
         return u''
 
     def _after(self, repo, commit_ids):
         if len(commit_ids) > 0:
-            return commit_ids[0]
+            return self._convert_id(commit_ids[0])
         return u''
 
+    def _convert_id(self, _id):
+        if ':' in _id:
+            _id = u'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(app.repo, commit_ids)
         payload = {


[22/37] allura git commit: [#4542] ticket:715 Add rate limits

Posted by je...@apache.org.
[#4542] ticket:715 Add rate limits


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

Branch: refs/heads/ib/4542
Commit: 5c82af9102913513a368f3c9db3adf5397e5f42e
Parents: e7ace57
Author: Igor Bondarenko <je...@gmail.com>
Authored: Mon Feb 2 12:01:13 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:38 2015 +0000

----------------------------------------------------------------------
 Allura/allura/model/webhook.py       | 23 ++++++++++++++++++-
 Allura/allura/tests/test_webhooks.py | 38 +++++++++++++++++++++++++++++++
 Allura/allura/webhooks.py            |  6 ++++-
 Allura/development.ini               |  6 +++++
 4 files changed, 71 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/5c82af91/Allura/allura/model/webhook.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/webhook.py b/Allura/allura/model/webhook.py
index 05eb436..09cc7fa 100644
--- a/Allura/allura/model/webhook.py
+++ b/Allura/allura/model/webhook.py
@@ -15,7 +15,12 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from ming.odm import FieldProperty
+import datetime as dt
+
+from ming.odm import FieldProperty, session
+from paste.deploy.converters import asint
+from tg import config
+
 from allura.model import Artifact
 
 
@@ -28,6 +33,7 @@ class Webhook(Artifact):
     type = FieldProperty(str)
     hook_url = FieldProperty(str)
     secret = FieldProperty(str)
+    last_sent = FieldProperty(dt.datetime, if_missing=None)
 
     def url(self):
         return '{}{}/{}/{}'.format(
@@ -41,3 +47,18 @@ class Webhook(Artifact):
         ac_ids = [ac._id for ac in project.app_configs]
         hooks = cls.query.find(dict(type=type, app_config_id={'$in': ac_ids}))
         return hooks.all()
+
+    def enforce_limit(self):
+        '''Returns False if limit is reached, otherwise True'''
+        if self.last_sent is None:
+            return True
+        now = dt.datetime.utcnow()
+        config_type = self.type.replace('-', '_')
+        limit = asint(config.get('webhook.%s.limit' % config_type, 30))
+        if (now - self.last_sent) > dt.timedelta(seconds=limit):
+            return True
+        return False
+
+    def update_limit(self):
+        self.last_sent = dt.datetime.utcnow()
+        session(self).flush(self)

http://git-wip-us.apache.org/repos/asf/allura/blob/5c82af91/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index fa0305f..99e5f91 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -1,6 +1,7 @@
 import json
 import hmac
 import hashlib
+import datetime as dt
 
 from mock import Mock, patch
 from nose.tools import (
@@ -12,6 +13,7 @@ from nose.tools import (
 from formencode import Invalid
 from ming.odm import session
 from pylons import tmpl_context as c
+from tg import config
 
 from allura import model as M
 from allura.lib import helpers as h
@@ -431,6 +433,18 @@ class TestRepoPushWebhookSender(TestWebhookBase):
             self.wh._id,
             sender.get_payload.return_value)
 
+    @patch('allura.webhooks.log', autospec=True)
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send_limit_reached(self, send_webhook, log):
+        sender = RepoPushWebhookSender()
+        sender.get_payload = Mock()
+        self.wh.enforce_limit = Mock(return_value=False)
+        with h.push_config(c, app=self.git):
+            sender.send(arg1=1, arg2=2)
+        assert_equal(send_webhook.post.call_count, 0)
+        log.warn.assert_called_once_with(
+            'Webhook fires too often: %s. Skipping', self.wh)
+
     @patch('allura.webhooks.send_webhook', autospec=True)
     def test_send_no_configured_webhooks(self, send_webhook):
         self.wh.delete()
@@ -467,3 +481,27 @@ class TestModels(TestWebhookBase):
     def test_webhook_url(self):
         assert_equal(self.wh.url(),
             '/adobe/adobe-1/admin/webhooks/repo-push/{}'.format(self.wh._id))
+
+    def test_webhook_enforce_limit(self):
+        self.wh.last_sent = None
+        assert_equal(self.wh.enforce_limit(), True)
+        # default value
+        self.wh.last_sent = dt.datetime.utcnow() - dt.timedelta(seconds=31)
+        assert_equal(self.wh.enforce_limit(), True)
+        self.wh.last_sent = dt.datetime.utcnow() - dt.timedelta(seconds=15)
+        assert_equal(self.wh.enforce_limit(), False)
+        # value from config
+        with h.push_config(config, **{'webhook.repo_push.limit': 100}):
+            self.wh.last_sent = dt.datetime.utcnow() - dt.timedelta(seconds=101)
+            assert_equal(self.wh.enforce_limit(), True)
+            self.wh.last_sent = dt.datetime.utcnow() - dt.timedelta(seconds=35)
+            assert_equal(self.wh.enforce_limit(), False)
+
+    @patch('allura.model.webhook.dt', autospec=True)
+    def test_update_limit(self, dt_mock):
+        _now = dt.datetime(2015, 02, 02, 13, 39)
+        dt_mock.datetime.utcnow.return_value = _now
+        assert_equal(self.wh.last_sent, None)
+        self.wh.update_limit()
+        session(self.wh).expunge(self.wh)
+        assert_equal(M.Webhook.query.get(_id=self.wh._id).last_sent, _now)

http://git-wip-us.apache.org/repos/asf/allura/blob/5c82af91/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 2393acd..e13086d 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -272,7 +272,11 @@ class WebhookSender(object):
         if webhooks:
             payload = self.get_payload(**kw)
             for webhook in webhooks:
-                send_webhook.post(webhook._id, payload)
+                if webhook.enforce_limit():
+                    webhook.update_limit()
+                    send_webhook.post(webhook._id, payload)
+                else:
+                    log.warn('Webhook fires too often: %s. Skipping', webhook)
 
 
 class RepoPushWebhookSender(WebhookSender):

http://git-wip-us.apache.org/repos/asf/allura/blob/5c82af91/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index c6425a8..39a9efa 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -129,6 +129,12 @@ user_prefs_storage.ldap.fields.display_name = cn
 # Limit the number of emails a user can claim.
 user_prefs.maximum_claimed_emails = 20
 
+# Limit rate of webhook firing (in seconds, default = 30)
+# Option format: webhook.<hook type>.limit,
+# all '-' in hook type must be changed to '_'
+# e.g. for repo-push webhook:
+# webhook.repo_push.limit = 10
+
 # Additional fields for admin project/user search
 # Note: whitespace after comma is important!
 # search.project.additional_search_fields = private, url, title


[32/37] allura git commit: [#4542] ticket:726 Fix git & svn admin menu tests

Posted by je...@apache.org.
[#4542] ticket:726 Fix git & svn admin menu tests


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

Branch: refs/heads/ib/4542
Commit: 289eed1fa5d62824c67b36d933380328124b8bed
Parents: 1ec6582
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Feb 11 16:19:36 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:41 2015 +0000

----------------------------------------------------------------------
 ForgeGit/forgegit/tests/test_git_app.py | 2 +-
 ForgeSVN/forgesvn/tests/test_svn_app.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/289eed1f/ForgeGit/forgegit/tests/test_git_app.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/test_git_app.py b/ForgeGit/forgegit/tests/test_git_app.py
index 41034b1..0566249 100644
--- a/ForgeGit/forgegit/tests/test_git_app.py
+++ b/ForgeGit/forgegit/tests/test_git_app.py
@@ -40,7 +40,7 @@ class TestGitApp(unittest.TestCase):
         ThreadLocalORMSession.close_all()
 
     def test_admin_menu(self):
-        assert_equals(len(c.app.admin_menu()), 6)
+        assert_equals(len(c.app.admin_menu()), 7)
 
     def test_uninstall(self):
         from allura import model as M

http://git-wip-us.apache.org/repos/asf/allura/blob/289eed1f/ForgeSVN/forgesvn/tests/test_svn_app.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/tests/test_svn_app.py b/ForgeSVN/forgesvn/tests/test_svn_app.py
index 7fd8545..6ea576a 100644
--- a/ForgeSVN/forgesvn/tests/test_svn_app.py
+++ b/ForgeSVN/forgesvn/tests/test_svn_app.py
@@ -40,7 +40,7 @@ class TestSVNApp(unittest.TestCase):
         ThreadLocalORMSession.close_all()
 
     def test_admin_menu(self):
-        assert_equals(len(c.app.admin_menu()), 6)
+        assert_equals(len(c.app.admin_menu()), 7)
         assert_equals(c.app.admin_menu()[0].label, 'Checkout URL')
 
     def test_uninstall(self):