You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2013/03/21 15:21:37 UTC

[01/48] git commit: [#5755] quick fix for ForgeHg icon not referencing right

Updated Branches:
  refs/heads/si/5453 16b7fb043 -> 2e6455f0a (forced update)


[#5755] quick fix for ForgeHg icon not referencing right


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

Branch: refs/heads/si/5453
Commit: 234ef740dca3fd6b2791aefef474aa6cc86e0b31
Parents: 6049825
Author: Dave Brondsema <db...@geek.net>
Authored: Fri Mar 8 22:54:13 2013 +0000
Committer: Dave Brondsema <db...@geek.net>
Committed: Fri Mar 8 22:54:13 2013 +0000

----------------------------------------------------------------------
 .../allura/ext/admin/templates/project_admin.html  |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/234ef740/Allura/allura/ext/admin/templates/project_admin.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/admin/templates/project_admin.html b/Allura/allura/ext/admin/templates/project_admin.html
index 4f140fd..7ea4052 100644
--- a/Allura/allura/ext/admin/templates/project_admin.html
+++ b/Allura/allura/ext/admin/templates/project_admin.html
@@ -42,7 +42,7 @@
   {% if scm_tools %}
     {% set tool = scm_tools[0] %}
     <div class="grid-2">
-        <img src="{{ g.theme.app_icon_url(tool.tool_name, 48) }}" alt="">
+        <img src="{{ g.theme.app_icon_url('Git', 48) }}" alt="">
     </div>
     <div class="grid-13">
       <h3>Code</h3>


[33/48] git commit: [5453] Implemented loop to load other listeners updating stats

Posted by tv...@apache.org.
[5453] Implemented loop to load other listeners updating stats


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

Branch: refs/heads/si/5453
Commit: 888638f94334b74db8a76a8cd4e08893318aed1e
Parents: 7fdca7a
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:03:56 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/app_globals.py |    8 ++++----
 Allura/development.ini           |    4 +++-
 2 files changed, 7 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/888638f9/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 71426a6..632a29f 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -172,12 +172,12 @@ class Globals(object):
         # Zarkov logger
         self._zarkov = None
 
-        self.show_userstats = config.get('user.stats.enable','false')=='true'
+        self.show_userstats = config.get('userstats.enable','false')=='true'
         # Set listeners to update stats
         statslisteners = []
-        ep = self.entry_points['stats'].get('userstats')
-        if self.show_userstats and ep:
-            statslisteners.append(ep().listener)
+        for name, ep in self.entry_points['stats'].iteritems():
+            if config.get('%s.enable' % name,'false')=='true':
+                statslisteners.append(ep().listener)
         self.statsUpdater = PostEvent(statslisteners)
 
     @LazyProperty

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/888638f9/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 7d2bbe0..68821e4 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -128,7 +128,9 @@ trovecategories.enableediting = true
 
 # If set to false, the stats of the user are not
 # updated and they are not shown to users.
-user.stats.enable = true
+# Note: the name of the parameter has to be the same
+#       of the entry point, followed by .enable
+userstats.enable = true
 
 # ActivityStream
 activitystream.master = mongodb://127.0.0.1:27017


[13/48] git commit: [5453] Added some tests

Posted by tv...@apache.org.
[5453] Added some tests


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

Branch: refs/heads/si/5453
Commit: 1723f17bf6eedd089b1dc2dcb9706d4b214eb743
Parents: 53e4e11
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 19:07:48 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 run_tests |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1723f17b/run_tests
----------------------------------------------------------------------
diff --git a/run_tests b/run_tests
index eb2bf3b..80a3079 100755
--- a/run_tests
+++ b/run_tests
@@ -22,6 +22,7 @@ if [ "$TEST_MODULES"  == "" ]; then
     ForgeWiki \
     ForgeActivity \
     ForgeShortUrl \
+    ForgeUserStats \
     "
 fi
 


[03/48] git commit: [#5752] ticket:294 fix commit refs in diff template for image diffs

Posted by tv...@apache.org.
[#5752] ticket:294 fix commit refs in diff template for image diffs


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

Branch: refs/heads/si/5453
Commit: 0e7d7f61394ee7b223fd5188a651ab74a12ad4db
Parents: 1b4ce94
Author: Yuriy Arhipov <yu...@yandex.ru>
Authored: Mon Mar 11 11:16:01 2013 +0400
Committer: Dave Brondsema <db...@geek.net>
Committed: Wed Mar 13 16:30:29 2013 +0000

----------------------------------------------------------------------
 Allura/allura/templates/repo/barediff.html         |    8 ++--
 Allura/allura/templates/repo/diff.html             |    8 ++--
 .../2c/e83a24e52c21e8d2146b1a04a20717c0bb08d7      |    2 +
 .../4d/a92902b500ae6b74b8e6dea9639ebfde6a007f      |  Bin 0 -> 158 bytes
 .../82/ac89a3fa20831c99faa54331fdce563df45dc8      |  Bin 0 -> 794 bytes
 .../8d/27123659c0022b0a4a7b10f4d1fe03800a1c8c      |  Bin 0 -> 158 bytes
 .../d9/61abbbf10341ee18a668c975842c35cfc0bef2      |    1 +
 .../e1/db0418f7fe2c16e9a57b50196e6ff25c2089ca      |  Bin 0 -> 415 bytes
 .../tests/data/testmime.git/refs/heads/master      |    2 +-
 .../forgegit/tests/functional/test_controllers.py  |   29 +++++++++++++++
 10 files changed, 41 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/Allura/allura/templates/repo/barediff.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/repo/barediff.html b/Allura/allura/templates/repo/barediff.html
index 4dfce67..24d8b48 100644
--- a/Allura/allura/templates/repo/barediff.html
+++ b/Allura/allura/templates/repo/barediff.html
@@ -1,10 +1,10 @@
 {% if a.has_image_view and b.has_image_view %}
   <img src="{{a.url()}}?format=raw"
-       alt="{{h.text.truncate(a._commit._id, 10)}}"
-       title="{{h.text.truncate(a._commit._id, 10)}}"/>
+       alt="{{h.text.truncate(a.commit._id, 10)}}"
+       title="{{h.text.truncate(a.commit._id, 10)}}"/>
   <img src="{{b.url()}}?format=raw"
-       alt="{{h.text.truncate(b._commit._id, 10)}}"
-       title="{{h.text.truncate(b._commit._id, 10)}}"/>
+       alt="{{h.text.truncate(b.commit._id, 10)}}"
+       title="{{h.text.truncate(b.commit._id, 10)}}"/>
 {% else %}
   {% if session.diformat == 'sidebyside' %}
     {{diff|safe}}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/Allura/allura/templates/repo/diff.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/repo/diff.html b/Allura/allura/templates/repo/diff.html
index 4c5ba93..8f83ef8 100644
--- a/Allura/allura/templates/repo/diff.html
+++ b/Allura/allura/templates/repo/diff.html
@@ -21,11 +21,11 @@
   {% if a.has_image_view and b.has_image_view %}
   <div class="grid-19">
     <img src="{{a.url()}}?format=raw"
-         alt="{{h.text.truncate(a._commit._id, 10)}}"
-         title="{{h.text.truncate(a._commit._id, 10)}}"/>
+         alt="{{h.text.truncate(a.commit._id, 10)}}"
+         title="{{h.text.truncate(a.commit._id, 10)}}"/>
     <img src="{{b.url()}}?format=raw"
-         alt="{{h.text.truncate(b._commit._id, 10)}}"
-         title="{{h.text.truncate(b._commit._id, 10)}}"/>
+         alt="{{h.text.truncate(b.commit._id, 10)}}"
+         title="{{h.text.truncate(b.commit._id, 10)}}"/>
   </div>
   {% else %}
   <div class="clip grid-19">

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/2c/e83a24e52c21e8d2146b1a04a20717c0bb08d7
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/2c/e83a24e52c21e8d2146b1a04a20717c0bb08d7 b/ForgeGit/forgegit/tests/data/testmime.git/objects/2c/e83a24e52c21e8d2146b1a04a20717c0bb08d7
new file mode 100644
index 0000000..76d9ad8
--- /dev/null
+++ b/ForgeGit/forgegit/tests/data/testmime.git/objects/2c/e83a24e52c21e8d2146b1a04a20717c0bb08d7
@@ -0,0 +1,2 @@
+x��An�0EY����fl�I$��ct9��!�q���z�.��}�/�4�6t�VUaH�'�B7
+���s	�O�݀�$�������ˢ���#r�_���Ǟ���D.^�u��ֲ�W��e����B���?7�'�=���c�{��=��wi��lN	�t���4}4`���Tr�yZn��<JVq
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/4d/a92902b500ae6b74b8e6dea9639ebfde6a007f
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/4d/a92902b500ae6b74b8e6dea9639ebfde6a007f b/ForgeGit/forgegit/tests/data/testmime.git/objects/4d/a92902b500ae6b74b8e6dea9639ebfde6a007f
new file mode 100644
index 0000000..ff64d9f
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testmime.git/objects/4d/a92902b500ae6b74b8e6dea9639ebfde6a007f differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/82/ac89a3fa20831c99faa54331fdce563df45dc8
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/82/ac89a3fa20831c99faa54331fdce563df45dc8 b/ForgeGit/forgegit/tests/data/testmime.git/objects/82/ac89a3fa20831c99faa54331fdce563df45dc8
new file mode 100644
index 0000000..802cdcb
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testmime.git/objects/82/ac89a3fa20831c99faa54331fdce563df45dc8 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/8d/27123659c0022b0a4a7b10f4d1fe03800a1c8c
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/8d/27123659c0022b0a4a7b10f4d1fe03800a1c8c b/ForgeGit/forgegit/tests/data/testmime.git/objects/8d/27123659c0022b0a4a7b10f4d1fe03800a1c8c
new file mode 100644
index 0000000..a08d240
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testmime.git/objects/8d/27123659c0022b0a4a7b10f4d1fe03800a1c8c differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/d9/61abbbf10341ee18a668c975842c35cfc0bef2
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/d9/61abbbf10341ee18a668c975842c35cfc0bef2 b/ForgeGit/forgegit/tests/data/testmime.git/objects/d9/61abbbf10341ee18a668c975842c35cfc0bef2
new file mode 100644
index 0000000..9a8f0e9
--- /dev/null
+++ b/ForgeGit/forgegit/tests/data/testmime.git/objects/d9/61abbbf10341ee18a668c975842c35cfc0bef2
@@ -0,0 +1 @@
+x��KN�0g�S�i��8�H#�`ٶ�K������	p�Y֢��u�k��қ�́RYlt&z�Y8�!H,Y,#��6n��b?0)��I5�aB�]��g�x����co���6�m�����O;��d�����`)��G�4�*��vyfC����@��PΚ.���%�RN��wQ?nnX�
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/objects/e1/db0418f7fe2c16e9a57b50196e6ff25c2089ca
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/objects/e1/db0418f7fe2c16e9a57b50196e6ff25c2089ca b/ForgeGit/forgegit/tests/data/testmime.git/objects/e1/db0418f7fe2c16e9a57b50196e6ff25c2089ca
new file mode 100644
index 0000000..62cab6a
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testmime.git/objects/e1/db0418f7fe2c16e9a57b50196e6ff25c2089ca differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/data/testmime.git/refs/heads/master
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testmime.git/refs/heads/master b/ForgeGit/forgegit/tests/data/testmime.git/refs/heads/master
index 5a06233..7e6b3d7 100644
--- a/ForgeGit/forgegit/tests/data/testmime.git/refs/heads/master
+++ b/ForgeGit/forgegit/tests/data/testmime.git/refs/heads/master
@@ -1 +1 @@
-6060fc5fce84b93113ba659c2c64971e134cd136
+d961abbbf10341ee18a668c975842c35cfc0bef2

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0e7d7f61/ForgeGit/forgegit/tests/functional/test_controllers.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/functional/test_controllers.py b/ForgeGit/forgegit/tests/functional/test_controllers.py
index 7dcb88c..930df8b 100644
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -387,3 +387,32 @@ class TestFork(_TestCase):
         r = self.app.post('/p/test/src-git/merge-requests/%s/save' % mr_num,
                           params=dict(status='rejected')).follow()
         assert 'Merge Request #%s:  (rejected)' % mr_num in r, r
+
+class TestDiff(TestController):
+
+    def setUp(self):
+        super(TestDiff, self).setUp()
+        self.setup_with_tools()
+
+    @with_git
+    def setup_with_tools(self):
+        h.set_context('test', 'src-git', neighborhood='Projects')
+        repo_dir = pkg_resources.resource_filename(
+            'forgegit', 'tests/data')
+        c.app.repo.fs_path = repo_dir
+        c.app.repo.status = 'ready'
+        c.app.repo.name = 'testmime.git'
+        ThreadLocalORMSession.flush_all()
+        h.set_context('test', 'src-git', neighborhood='Projects')
+        c.app.repo.refresh()
+        ThreadLocalORMSession.flush_all()
+
+    def test_diff(self):
+        r = self.app.get('/src-git/ci/d961abbbf10341ee18a668c975842c35cfc0bef2/tree/1.png?barediff=2ce83a24e52c21e8d2146b1a04a20717c0bb08d7')
+        assert 'alt="2ce83a2..."' in r
+        assert 'alt="d961abb..."' in r
+
+        r = self.app.get('/src-git/ci/d961abbbf10341ee18a668c975842c35cfc0bef2/tree/1.png?diff=2ce83a24e52c21e8d2146b1a04a20717c0bb08d7')
+        assert 'alt="2ce83a2..."' in r
+        assert 'alt="d961abb..."' in r
+


[06/48] git commit: [#5909] Fixed URLs in auth tests

Posted by tv...@apache.org.
[#5909] Fixed URLs in auth tests

Signed-off-by: Cory Johns <jo...@geek.net>


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

Branch: refs/heads/si/5453
Commit: ca6d9ea9b762538b60288e906b2b1506e3b5a01c
Parents: 9d3caf3
Author: Cory Johns <jo...@geek.net>
Authored: Wed Mar 13 20:07:39 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:47 2013 +0000

----------------------------------------------------------------------
 Allura/allura/tests/functional/test_auth.py |   58 +++++++++++-----------
 1 files changed, 29 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/ca6d9ea9/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 666c3ec..5a45909 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -382,7 +382,7 @@ class TestPreferences(TestController):
         from pytz import country_names
         setsex, setbirthdate, setcountry, setcity, settimezone = \
             ('Male', '19/08/1988', 'IT', 'Milan', 'Europe/Rome')
-        result = self.app.get('/auth/user_info')
+        result = self.app.get('/auth/user_info/')
 
         #Check if personal data is properly set
         r = self.app.post('/auth/user_info/change_personal_data',
@@ -435,7 +435,7 @@ class TestPreferences(TestController):
     def test_contacts(self):
         #Add skype account
         testvalue = 'testaccount'
-        result = self.app.get('/auth/user_info/contacts')
+        result = self.app.get('/auth/user_info/contacts/')
         r = self.app.post('/auth/user_info/contacts/skype_account',
              params=dict(skypeaccount=testvalue))
         user = M.User.query.get(username='test-admin')
@@ -444,7 +444,7 @@ class TestPreferences(TestController):
         #Add social network account
         socialnetwork = 'Facebook'
         accounturl = 'http://www.facebook.com/test'
-        r = self.app.post('/auth/preferences/contacts/add_social_network',
+        r = self.app.post('/auth/user_info/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork,
                          accounturl = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -455,7 +455,7 @@ class TestPreferences(TestController):
         #Add second social network account
         socialnetwork2 = 'Twitter'
         accounturl2 = 'http://twitter.com/test'
-        r = self.app.post('/auth/preferences/contacts/add_social_network',
+        r = self.app.post('/auth/user_info/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork2,
                          accounturl = '@test'))
         user = M.User.query.get(username='test-admin')
@@ -464,7 +464,7 @@ class TestPreferences(TestController):
                 {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks)
 
         #Remove first social network account
-        r = self.app.post('/auth/preferences/contacts/remove_social_network',
+        r = self.app.post('/auth/user_info/contacts/remove_social_network',
              params=dict(socialnetwork=socialnetwork,
                          account = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -472,7 +472,7 @@ class TestPreferences(TestController):
                {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks
 
         #Add invalid social network account
-        r = self.app.post('/auth/preferences/contacts/add_social_network',
+        r = self.app.post('/auth/user_info/contacts/add_social_network',
              params=dict(accounturl = accounturl, socialnetwork=''))
         user = M.User.query.get(username='test-admin')
         assert len(user.socialnetworks) == 1 and \
@@ -480,40 +480,40 @@ class TestPreferences(TestController):
 
         #Add telephone number
         telnumber = '+3902123456'
-        r = self.app.post('/auth/preferences/contacts/add_telnumber',
+        r = self.app.post('/auth/user_info/contacts/add_telnumber',
              params=dict(newnumber=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and (user.telnumbers[0] == telnumber))
 
         #Add second telephone number
         telnumber2 = '+3902654321'
-        r = self.app.post('/auth/preferences/contacts/add_telnumber',
+        r = self.app.post('/auth/user_info/contacts/add_telnumber',
              params=dict(newnumber=telnumber2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 2 and telnumber in user.telnumbers and telnumber2 in user.telnumbers)
 
         #Remove first telephone number
-        r = self.app.post('/auth/preferences/contacts/remove_telnumber',
+        r = self.app.post('/auth/user_info/contacts/remove_telnumber',
              params=dict(oldvalue=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and telnumber2 in user.telnumbers)
 
         #Add website
         website = 'http://www.testurl.com'
-        r = self.app.post('/auth/preferences/contacts/add_webpage',
+        r = self.app.post('/auth/user_info/contacts/add_webpage',
              params=dict(newwebsite=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and (website in user.webpages))
 
         #Add second website
         website2 = 'http://www.testurl2.com'
-        r = self.app.post('/auth/preferences/contacts/add_webpage',
+        r = self.app.post('/auth/user_info/contacts/add_webpage',
              params=dict(newwebsite=website2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 2 and website in user.webpages and website2 in user.webpages)
 
         #Remove first website
-        r = self.app.post('/auth/preferences/contacts/remove_webpage',
+        r = self.app.post('/auth/user_info/contacts/remove_webpage',
              params=dict(oldvalue=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and website2 in user.webpages)
@@ -527,8 +527,8 @@ class TestPreferences(TestController):
         starttime = time(9,0,0)
         endtime = time(12, 0, 0)
 
-        result = self.app.get('/auth/preferences/availability')
-        r = self.app.post('/auth/preferences/availability/add_timeslot',
+        result = self.app.get('/auth/user_info/availability/')
+        r = self.app.post('/auth/user_info/availability/add_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -542,7 +542,7 @@ class TestPreferences(TestController):
         endtime2 = time(16, 0, 0)
 
         #Add second availability timeslot
-        r = self.app.post('/auth/preferences/availability/add_timeslot',
+        r = self.app.post('/auth/user_info/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=starttime2.strftime('%H:%M'),
@@ -553,7 +553,7 @@ class TestPreferences(TestController):
                and timeslot2dict in user.get_availability_timeslots()
 
         #Remove availability timeslot
-        r = self.app.post('/auth/preferences/availability/remove_timeslot',
+        r = self.app.post('/auth/user_info/availability/remove_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -562,7 +562,7 @@ class TestPreferences(TestController):
         assert len(user.availability) == 1 and timeslot2dict in user.get_availability_timeslots()
 
         #Add invalid availability timeslot
-        r = self.app.post('/auth/preferences/availability/add_timeslot',
+        r = self.app.post('/auth/user_info/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=endtime2.strftime('%H:%M'),
@@ -581,8 +581,8 @@ class TestPreferences(TestController):
         now = datetime(now.year, now.month, now.day)
         startdate = now + timedelta(days=1)
         enddate = now + timedelta(days=7)
-        result = self.app.get('/auth/preferences/availability')
-        r = self.app.post('/auth/preferences/availability/add_inactive_period',
+        result = self.app.get('/auth/user_info/availability/')
+        r = self.app.post('/auth/user_info/availability/add_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -593,7 +593,7 @@ class TestPreferences(TestController):
         #Add second inactivity period
         startdate2 =  now + timedelta(days=24)
         enddate2 = now + timedelta(days=28)
-        r = self.app.post('/auth/preferences/availability/add_inactive_period',
+        r = self.app.post('/auth/user_info/availability/add_inactive_period',
              params=dict(
                  startdate=startdate2.strftime('%d/%m/%Y'),
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -603,7 +603,7 @@ class TestPreferences(TestController):
                and period2dict in user.get_inactive_periods()
 
         #Remove first inactivity period
-        r = self.app.post('/auth/preferences/availability/remove_inactive_period',
+        r = self.app.post('/auth/user_info/availability/remove_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -611,7 +611,7 @@ class TestPreferences(TestController):
         assert len(user.inactiveperiod) == 1 and period2dict in user.get_inactive_periods()
 
         #Add invalid inactivity period
-        r = self.app.post('/auth/preferences/availability/add_inactive_period',
+        r = self.app.post('/auth/user_info/availability/add_inactive_period',
              params=dict(
                  startdate='NOT/A/DATE',
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -627,8 +627,8 @@ class TestPreferences(TestController):
         skill_cat = M.TroveCategory.query.get(show_as_skill=True)
         level = 'low'
         comment = 'test comment'
-        result = self.app.get('/auth/preferences/skills')
-        r = self.app.post('/auth/preferences/skills/save_skill',
+        result = self.app.get('/auth/user_info/skills/')
+        r = self.app.post('/auth/user_info/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -640,8 +640,8 @@ class TestPreferences(TestController):
         #Add again the same skill
         level = 'medium'
         comment = 'test comment 2'
-        result = self.app.get('/auth/preferences/skills')
-        r = self.app.post('/auth/preferences/skills/save_skill',
+        result = self.app.get('/auth/user_info/skills/')
+        r = self.app.post('/auth/user_info/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -653,7 +653,7 @@ class TestPreferences(TestController):
         #Add an invalid skill
         level2 = 'not a level'
         comment2 = 'test comment 2'
-        r = self.app.post('/auth/preferences/skills/save_skill',
+        r = self.app.post('/auth/user_info/skills/save_skill',
              params=dict(
                  level=level2,
                  comment=comment2,
@@ -663,8 +663,8 @@ class TestPreferences(TestController):
         assert len(user.skills) == 1 and skilldict in user.skills
 
         #Remove a skill
-        result = self.app.get('/auth/preferences/skills')
-        r = self.app.post('/auth/preferences/skills/remove_skill',
+        result = self.app.get('/auth/user_info/skills/')
+        r = self.app.post('/auth/user_info/skills/remove_skill',
              params=dict(
                  categoryid=str(skill_cat.trove_cat_id)))
         user = M.User.query.get(username='test-admin')


[30/48] git commit: [5453] Improved functional tests

Posted by tv...@apache.org.
[5453] Improved functional tests


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

Branch: refs/heads/si/5453
Commit: 17275bba241cf0096cc783cbb74d0b4ccd0f459b
Parents: eea9aa6
Author: Stefano Invernizzi <st...@apache.org>
Authored: Tue Jan 22 11:20:26 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/tests/test_stats.py |   67 ++++++++++------
 1 files changed, 42 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/17275bba/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
index e4954b7..4389cd7 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_stats.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -16,6 +16,11 @@ from forgetracker import model as TM
 
 class TestStats(TestController):
 
+    def setUp(self):
+        super(TestStats, self).setUp()
+        p = M.Project.query.get(shortname='test')
+        p.add_user(M.User.by_username('test-user'), ['Admin'])
+
     def test_login(self):
         user = User.by_username('test-user')
         init_logins = c.user.stats.tot_logins_count
@@ -25,15 +30,14 @@ class TestStats(TestController):
         assert user.stats.tot_logins_count == 1 + init_logins
         assert user.stats.getLastMonthLogins() == 1 + init_logins
 
-    @td.with_user_project('test-admin')
-    @td.with_wiki
+    @td.with_tool('test', 'wiki', mount_point='wiki', mount_label='wiki', username='test-admin')
     def test_wiki_stats(self):
         initial_artifacts = c.user.stats.getArtifacts()
         initial_wiki = c.user.stats.getArtifacts(art_type="Wiki")
 
-        h.set_context('test', 'wiki', neighborhood='Projects')
-        page = WM.Page(title="TestPage", text="some text")
-        page.commit()
+        self.app.post('/wiki/TestPage/update', 
+            params=dict(title='TestPage', text='some text'),
+            extra_environ=dict(username=str(c.user.username)))
 
         artifacts = c.user.stats.getArtifacts()
         wiki = c.user.stats.getArtifacts(art_type="Wiki")
@@ -43,8 +47,9 @@ class TestStats(TestController):
         assert wiki['created'] == 1 + initial_wiki['created']
         assert wiki['modified'] == initial_wiki['modified']
 
-        page = WM.Page(title="TestPage2", text="some different text")
-        page.commit()
+        self.app.post('/wiki/TestPage2/update', 
+            params=dict(title='TestPage2', text='some text'),
+            extra_environ=dict(username=str(c.user.username)))
 
         artifacts = c.user.stats.getArtifacts()
         wiki = c.user.stats.getArtifacts(art_type="Wiki")
@@ -54,9 +59,9 @@ class TestStats(TestController):
         assert wiki['created'] == 2 + initial_wiki['created']
         assert wiki['modified'] == initial_wiki['modified']
 
-
-        page.text="some modified text"
-        page.commit()
+        self.app.post('/wiki/TestPage2/update', 
+            params=dict(title='TestPage2', text='some modified text'),
+            extra_environ=dict(username=str(c.user.username)))
 
         artifacts = c.user.stats.getArtifacts()
         wiki = c.user.stats.getArtifacts(art_type="Wiki")
@@ -66,16 +71,17 @@ class TestStats(TestController):
         assert wiki['created'] == 2 + initial_wiki['created']
         assert wiki['modified'] == 1 + initial_wiki['modified']
 
-
-    @td.with_user_project('test-admin')
-    @td.with_tracker
+    @td.with_tool('test', 'tickets', mount_point='tickets', mount_label='tickets', username='test-admin')
     def test_tracker_stats(self):
         initial_tickets = c.user.stats.getTickets()
         initial_tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
 
-        h.set_context('test', 'bugs', neighborhood='Projects')
-        ticket = TM.Ticket(ticket_num=12, summary="test", assigned_to_id = c.user._id)
-        ticket.commit()
+        r = self.app.post('/tickets/save_ticket', 
+            params={'ticket_form.summary':'test',
+                    'ticket_form.assigned_to' : str(c.user.username)},
+            extra_environ=dict(username=str(c.user.username)))
+
+        ticketnum = str(TM.Ticket.query.get(summary='test').ticket_num)
 
         tickets = c.user.stats.getTickets()
         tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
@@ -86,8 +92,11 @@ class TestStats(TestController):
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 1
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified']
 
-        ticket.status = 'closed'
-        ticket.commit()
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticketnum, 
+            params={'ticket_form.ticket_num' : ticketnum,
+                    'ticket_form.summary':'footext3',
+                    'ticket_form.status' : 'closed'},
+            extra_environ=dict(username=str(c.user.username)))
 
         tickets = c.user.stats.getTickets()
         tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
@@ -98,9 +107,11 @@ class TestStats(TestController):
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 1
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 1
 
-        h.set_context('test', 'bugs', neighborhood='Projects')
-        ticket = TM.Ticket(ticket_num=13, summary="test")
-        ticket.commit()
+        r = self.app.post('/tickets/save_ticket', 
+            params={'ticket_form.summary':'test2'},
+            extra_environ=dict(username=str(c.user.username)))
+
+        ticketnum = str(TM.Ticket.query.get(summary='test2').ticket_num)
         
         tickets = c.user.stats.getTickets()
         tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
@@ -111,8 +122,11 @@ class TestStats(TestController):
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 1
 
-        ticket.assigned_to_id = c.user._id
-        ticket.commit()
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticketnum, 
+            params={'ticket_form.ticket_num' : ticketnum,
+                    'ticket_form.summary':'test2',
+                    'ticket_form.assigned_to' : str(c.user.username)},
+            extra_environ=dict(username=str(c.user.username)))
 
         tickets = c.user.stats.getTickets()
         tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
@@ -123,8 +137,11 @@ class TestStats(TestController):
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 2
 
-        ticket.assigned_to_id = User.by_username('test-user')._id
-        ticket.commit()
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticketnum, 
+            params={'ticket_form.ticket_num' : ticketnum,
+                    'ticket_form.summary':'test2',
+                    'ticket_form.assigned_to' : 'test-user'},
+            extra_environ=dict(username=str(c.user.username)))
 
         tickets = c.user.stats.getTickets()
         tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")


[43/48] git commit: [#5453] Fix svn test to work with user stats

Posted by tv...@apache.org.
[#5453] Fix svn test to work with user stats

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


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

Branch: refs/heads/si/5453
Commit: f0ec0bea51d74638dd39e23f3dc8cd35f4f2687b
Parents: 406dc65
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 12 20:04:01 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py             |    2 +-
 ForgeSVN/forgesvn/tests/model/test_repository.py |   14 +++++++++-----
 2 files changed, 10 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f0ec0bea/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index 5a4751a..3799ea5 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -16,7 +16,7 @@ from allura.lib import helpers as h
 
 class Stats(MappedClass):
     class __mongometa__:
-        name='userstats'
+        name='basestats'
         session = main_orm_session
         unique_indexes = [ '_id']
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f0ec0bea/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 2b02ad0..36c68a6 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -518,8 +518,12 @@ class TestRepo(_TestWithRepo):
         assert self.repo.guess_type('.gitignore') == ('text/plain', None)
 
     def test_refresh(self):
+        committer_name = 'Test Committer'
+        committer_email = 'test@example.com'
         ci = mock.Mock()
-        ci.authored.name = 'Test Committer'
+        ci.authored.name = committer_name
+        ci.committed.name = committer_name
+        ci.committed.email = committer_email
         ci.author_url = '/u/test-committer/'
         self.repo.count_revisions=mock.Mock(return_value=100)
         self.repo._impl.commit = mock.Mock(return_value=ci)
@@ -529,8 +533,8 @@ class TestRepo(_TestWithRepo):
         def refresh_commit_info(oid, seen, lazy=False):
             M.repo.CommitDoc(dict(
                     authored=dict(
-                        name='Test Committer',
-                        email='test@test.com'),
+                        name=committer_name,
+                        email=committer_email),
                     _id=oid)).m.insert()
         def set_heads():
             self.repo.heads = [ ming.base.Object(name='head', object_id='foo0', count=100) ]
@@ -544,13 +548,13 @@ class TestRepo(_TestWithRepo):
         notifications = M.Notification.query.find().all()
         for n in notifications:
             if '100 new commits' in n.subject:
-                assert "master,branch:  by Test Committer http://localhost/ci/foo99" in n.text
+                assert "master,branch:  by %s http://localhost/ci/foo99" % committer_name in n.text
                 break
         else:
             assert False, 'Did not find notification'
         assert M.Feed.query.find(dict(
             title='New commit',
-            author_name='Test Committer')).count()
+            author_name=committer_name)).count()
 
     def test_refresh_private(self):
         ci = mock.Mock()


[24/48] git commit: [5453] correct links in userstats

Posted by tv...@apache.org.
[5453] correct links in userstats


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

Branch: refs/heads/si/5453
Commit: 453f979ddbb37b6c377cb2529daded5bfc6e2bd3
Parents: 28d50bb
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 31 15:57:12 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/templates/index.html |   11 +++++++----
 1 files changed, 7 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/453f979d/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index abcb5de..37cd5da 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -13,6 +13,11 @@
 {% block content %}
   {% if user %}
 
+    {% if category %}
+       <ul>
+         <li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li>
+       </ul>
+    {% endif %}
     <h2>General statistics</h2>
     <table>
       <thead>
@@ -352,7 +357,7 @@
           <tbody>
             {% for cat, count in categories %}
               <tr>
-                <td><a href="/userstats/{{user.username}}/category/{{cat.fullname}}">{{cat.fullname}}</a></td>
+                <td><a href="/userstats/{{user.username}}/category/{{cat.shortname}}">{{cat.fullname}}</a></td>
                 <td>{{count}}</td>
               </tr>
             {% endfor %}
@@ -368,9 +373,7 @@
           </p>
         {% endif %}
     {% endif %}
-    {% if category %}
-        <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
-    {% else %}
+    {% if not category %}
       <h2>Overview</h2>
       <table>
         <thead>


[09/48] git commit: [#4808] change sf.net git repo to apache incubator

Posted by tv...@apache.org.
[#4808] change sf.net git repo to apache incubator


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

Branch: refs/heads/si/5453
Commit: f5df3c46a8059e751083e5b7517bb49d564e56ea
Parents: ca6d9ea
Author: Dave Brondsema <db...@geek.net>
Authored: Sat Mar 9 13:25:12 2013 -0500
Committer: Dave Brondsema <db...@geek.net>
Committed: Wed Mar 13 16:29:27 2013 -0400

----------------------------------------------------------------------
 README.markdown |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f5df3c46/README.markdown
----------------------------------------------------------------------
diff --git a/README.markdown b/README.markdown
index b60d890..775b97a 100644
--- a/README.markdown
+++ b/README.markdown
@@ -54,7 +54,7 @@ Now we can get down to actually getting the Forge code and dependencies download
 
     (anvil)~$ mkdir src
     (anvil)~$ cd src
-    (anvil)~/src$ git clone git://git.code.sf.net/p/allura/git.git forge
+    (anvil)~/src$ git clone https://git-wip-us.apache.org/repos/asf/incubator-allura.git forge
 
 Although the application setup.py files define a number of dependencies, the `requirements.txt` files are currently the authoritative source, so we'll use those with `pip` to make sure the correct versions are installed.
 


[41/48] git commit: [#5453] Fixed bug in userstats model

Posted by tv...@apache.org.
[#5453] Fixed bug in userstats model


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

Branch: refs/heads/si/5453
Commit: 406dc65f49732e9487a68b48f920f43545b35d39
Parents: ea7b708
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Mar 9 16:34:02 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py |    6 ++++--
 1 files changed, 4 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/406dc65f/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 8575e79..4171d52 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -25,8 +25,10 @@ class UserStats(Stats):
         stats = cls(user_id=user._id,
             registration_date = datetime.utcnow())
         user.stats_id = stats._id
-        session(stats).flush(stats)
-        session(user).flush(user)
+        if session(stats):
+            session(stats).flush(stats)
+        if session(user):
+            session(user).flush(user)
         return stats
 
     def getLastMonthLogins(self):


[44/48] git commit: [#5453] Avoid conflict with mongo's built-in stats collection

Posted by tv...@apache.org.
[#5453] Avoid conflict with mongo's built-in stats collection


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

Branch: refs/heads/si/5453
Commit: 6e8b9376b41b081d2e5d75ce6e433eea23f46b05
Parents: 85f83c2
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 5 21:45:38 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6e8b9376/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index 4d9e1e9..adc36db 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -16,7 +16,7 @@ from allura.lib import helpers as h
 
 class Stats(MappedClass):
     class __mongometa__:
-        name='stats'
+        name='userstats'
         session = main_orm_session
         unique_indexes = [ '_id']
 


[23/48] git commit: [5453] correct typo error

Posted by tv...@apache.org.
[5453] correct typo error


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

Branch: refs/heads/si/5453
Commit: 0b5e176698ed94b605e64baf4bf31f4f6a1063dd
Parents: 888638f
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:25:36 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/templates/index.html |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0b5e1766/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index 6d6a25b..abcb5de 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -338,7 +338,7 @@
     </table>
 
     {% if categories %}
-        <h2>Prefered categories</h2>
+        <h2>Preferred categories</h2>
         <p>
           The following table shows the number projects tagged as belonging to each single category in which this user is involved.
         </p>


[21/48] git commit: [5453] added missing import

Posted by tv...@apache.org.
[5453] added missing import


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

Branch: refs/heads/si/5453
Commit: a326694962ba14a3eef33fdcd3ace36f8bccba80
Parents: 25f7657
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Dec 12 22:32:29 2012 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/a3266949/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 5e4f21f..1993f57 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -19,6 +19,7 @@ from allura.model.repo import CommitDoc, TreeDoc, TreesDoc, DiffInfoDoc
 from allura.model.repo import LastCommitDoc, CommitRunDoc
 from allura.model.repo import Commit, Tree, LastCommit, ModelCache
 from allura.model.index import ArtifactReferenceDoc, ShortlinkDoc
+from allura.model.auth import User
 
 log = logging.getLogger(__name__)
 


[26/48] git commit: [5453] Improved tests

Posted by tv...@apache.org.
[5453] Improved tests


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

Branch: refs/heads/si/5453
Commit: 794ae631051f1484db3769ab88b35ea71c0b3c5e
Parents: 1528fc9
Author: Stefano Invernizzi <st...@apache.org>
Authored: Mon Jan 14 14:43:56 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/tests/test_stats.py |   60 ++++++---------
 ForgeUserStats/test.ini                           |    1 +
 2 files changed, 25 insertions(+), 36 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/794ae631/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
index 2972a0e..a3e3482 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_stats.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -16,25 +16,16 @@ from forgetracker import model as TM
 
 class TestStats(TestController):
 
-    test_username = 'teststats'
-    test_password = 'foo'
-
-    def setUp(self):
-        super(TestStats, self).setUp()
-        for ep in pkg_resources.iter_entry_points("allura.stats"):
-            if ep.name.lower() == 'userstats':
-                g.statslisteners = [ep.load()().listener]
-
-        self.user = User.register(dict(username=self.test_username,
+    @td.with_user_project('test-user')
+    def test_init_values(self):
+        user = User.register(dict(username='test-new-user',
             display_name='Test Stats'),
             make_project=False)
-        self.user.set_password(self.test_password)
-        
-    def test_init_values(self):
-        artifacts = self.user.stats.getArtifacts()
-        tickets = self.user.stats.getTickets()
-        commits = self.user.stats.getCommits()
-        assert self.user.stats.tot_logins_count == 0
+
+        artifacts = user.stats.getArtifacts()
+        tickets = user.stats.getTickets()
+        commits = user.stats.getCommits()
+        assert user.stats.tot_logins_count == 0
         assert artifacts['created'] == 0
         assert artifacts['modified'] == 0
         assert tickets['assigned'] == 0
@@ -44,10 +35,10 @@ class TestStats(TestController):
         assert commits['number'] == 0
         assert commits['lines'] == 0
 
-        lmartifacts = self.user.stats.getLastMonthArtifacts()
-        lmtickets = self.user.stats.getLastMonthTickets()
-        lmcommits = self.user.stats.getLastMonthCommits()
-        assert self.user.stats.getLastMonthLogins() == 0
+        lmartifacts = user.stats.getLastMonthArtifacts()
+        lmtickets = user.stats.getLastMonthTickets()
+        lmcommits = user.stats.getLastMonthCommits()
+        assert user.stats.getLastMonthLogins() == 0
         assert lmartifacts['created'] == 0
         assert lmartifacts['modified'] == 0
         assert lmtickets['assigned'] == 0
@@ -58,12 +49,13 @@ class TestStats(TestController):
         assert lmcommits['lines'] == 0
 
     def test_login(self):
-        init_logins = self.user.stats.tot_logins_count
+        user = User.by_username('test-user')
+        init_logins = c.user.stats.tot_logins_count
         r = self.app.post('/auth/do_login', params=dict(
-                username=self.test_username, password=self.test_password))
+                username=user.username, password='foo'))
 
-        assert self.user.stats.tot_logins_count == 1 + init_logins
-        assert self.user.stats.getLastMonthLogins() == 1 + init_logins
+        assert user.stats.tot_logins_count == 1 + init_logins
+        assert user.stats.getLastMonthLogins() == 1 + init_logins
 
     @td.with_user_project('test-admin')
     @td.with_wiki
@@ -163,7 +155,7 @@ class TestStats(TestController):
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 2
 
-        ticket.assigned_to_id = self.user._id
+        ticket.assigned_to_id = User.by_username('test-user')._id
         ticket.commit()
 
         tickets = c.user.stats.getTickets()
@@ -179,16 +171,11 @@ class TestGitCommit(unittest.TestCase, TestController):
 
     def setUp(self):
         setup_basic_test()
-        for ep in pkg_resources.iter_entry_points("allura.stats"):
-            if ep.name.lower() == 'userstats':
-                g.statslisteners = [ep.load()().listener]
 
-        self.user = User.register(dict(username='testuser',
-            display_name='Test'),
-            make_project=False)
-        self.user.set_password('testpassword')
+        user = User.by_username('test-admin')
+        user.set_password('testpassword')
         addr = M.EmailAddress.upsert('rcopeland@geek.net')
-        self.user.claim_address('rcopeland@geek.net')
+        user.claim_address('rcopeland@geek.net')
         self.setup_with_tools()
 
     @with_git
@@ -205,9 +192,10 @@ class TestGitCommit(unittest.TestCase, TestController):
         self.rev = M.repo.Commit.query.get(_id=self.repo.heads[0]['object_id'])
         self.rev.repo = self.repo
 
+    @td.with_user_project('test-admin')
     def test_commit(self):
-        commits = self.user.stats.getCommits()
+        commits = c.user.stats.getCommits()
         assert commits['number'] == 4
-        lmcommits = self.user.stats.getLastMonthCommits()
+        lmcommits = c.user.stats.getLastMonthCommits()
         assert lmcommits['number'] == 4
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/794ae631/ForgeUserStats/test.ini
----------------------------------------------------------------------
diff --git a/ForgeUserStats/test.ini b/ForgeUserStats/test.ini
index 6753aa4..ca15ecd 100644
--- a/ForgeUserStats/test.ini
+++ b/ForgeUserStats/test.ini
@@ -5,6 +5,7 @@
 #
 [DEFAULT]
 debug = true
+user.stats.enable = true
 
 [server:main]
 use = egg:Paste#http


[18/48] [5453] adding support for user stats

Posted by tv...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
new file mode 100644
index 0000000..0b3cfb8
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
@@ -0,0 +1,48 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Artifacts
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Created artifacts</th>
+            <th>Modified artifacts</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, row in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.created}}<br/>
+                {% endfor %}
+              </td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.modified}}<br/>
+                {% endfor %}
+              </td>
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
new file mode 100644
index 0000000..c574c9f
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
@@ -0,0 +1,37 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Code contribution
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Number of commits</th>
+            <th>Lines of code</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.number}}</td>
+              <td>{{el.lines}}</td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
new file mode 100644
index 0000000..b53e596
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
@@ -0,0 +1,341 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution
+    {% if category %}
+        in projects of category {{category.fullname}}
+    {% endif %}
+{% endblock %}
+
+{% block content %}
+  {% if user %}
+
+    <h2>General statistics</h2>
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Value</th>
+        </tr>
+      </thead>
+      <tbody> 
+        <tr>
+          <td>Registration date</td>
+          <td>
+            {{registration_date.strftime("%d %b %Y, %H:%M:%S (UTC)")}}, 
+            {{days}} day{% if days != 1 %}s{% endif %} ago</td>
+        </tr>
+        {% if last_login %}
+          <tr>
+            <td>Last login</td>
+            <td>
+              {{last_login.strftime("%d %b %Y, %H:%M:%S (UTC)")}},
+              {{last_login_days}} day{% if last_login_days != 1 %}s{% endif %} ago</td>
+            </td>
+          </tr>
+        {% endif %}
+      </tbody>
+    </table>
+
+    <h2>Contribution statistics</h2>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Total value</th>
+          <th>Average per-month value</th>
+          <th>Last 30 days</th>
+          {% if days >= 30 %}
+            <th>Trend</th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        {% if not category %}
+          <tr>
+            <td>Logins</td>
+            <td>{{totlogins}}</td>
+            <td>{{permonthlogins}}</td>
+            <td>{{lastmonth_logins}}</td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_logins > permonthlogins %}
+                  Up
+                {% elif lastmonth_logins == permonthlogins %}
+                  =
+                {% else %}
+                  Down
+                {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endif %}
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Commits number</a></td>
+          <td>{{totcommits.number}}</td>
+          <td>{{permonthcommits.number}}</td>
+          <td>{{lastmonthcommits.number}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if permonthcommits.number > permonthcommits.number %}
+                Up
+              {% elif permonthcommits.number == permonthcommits.number %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Added/modified LOCs</a></td>
+          <td>{{totcommits.lines}}</td>
+          <td>{{permonthcommits.lines}}</td>
+          <td>{{lastmonthcommits.lines}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if permonthcommits.lines > permonthcommits.lines %}
+                Up
+              {% elif permonthcommits.lines == permonthcommits.lines %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/artifacts/">Total number of created artifacts</a></td>
+          <td>{{totartifacts.created}}</td>
+          <td>{{permonthartifacts.created}}</td>
+          <td>{{lastmonthartifacts.created}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthartifacts.created > permonthartifacts.created %}
+                Up
+              {% elif lastmonthartifacts.created == permonthartifacts.created %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Total number of edited artifacts</a></td>
+          <td>{{totartifacts.modified}}</td>
+          <td>{{permonthartifacts.modified}}</td>
+          <td>{{lastmonthartifacts.modified}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthartifacts.modified > permonthartifacts.modified %}
+                Up
+              {% elif lastmonthartifacts.modified == permonthartifacts.modified %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        
+        {% for key, value in artifacts_by_type.items() %}
+          <tr>
+            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Created {{key}} artifacts</a></td>
+            <td>{{value.created}}</td>
+            <td>{{value.pmcreated}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].created}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].created > value.pmcreated %}
+                    Up
+                  {% elif lastmonth_artifacts_by_type[key].created == value.pmcreated %}
+                    =
+                  {% else %}
+                    Down
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+          <tr>
+            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Edited {{key}} artifacts</a></td>
+            <td>{{value.modified}}</td>
+            <td>{{value.pmmodified}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].modified}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].modified > value.pmmodified %}
+                    Up
+                  {% elif lastmonth_artifacts_by_type[key].modified == value.pmmodified %}
+                    =
+                  {% else %}
+                    Down
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endfor %}
+
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Assigned tickets</a></td>
+          <td>{{tottickets.assigned}}</td>
+          <td>{{permonthtickets.assigned}}</td>
+          <td>{{lastmonthtickets.assigned}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.assigned > permonthtickets.assigned %}
+                Up
+              {% elif lastmonthtickets.assigned == permonthtickets.assigned %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Revoked tickets</a></td>
+          <td>{{tottickets.revoked}}</td>
+          <td>{{permonthtickets.revoked}}</td>
+          <td>{{lastmonthtickets.revoked}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.revoked > permonthtickets.revoked %}
+                Up
+              {% elif lastmonthtickets.revoked == permonthtickets.revoked %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Solved tickets</a></td>
+          <td>{{tottickets.solved}}</td>
+          <td>{{permonthtickets.solved}}</td>
+          <td>{{lastmonthtickets.solved}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.solved > permonthtickets.solved %}
+                Up
+              {% elif lastmonthtickets.solved == permonthtickets.solved %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Average tickets solving time</a></td>
+          <td>
+            {% if tottickets.averagesolvingtime %}
+              {{tottickets.averagesolvingtime.days}} days, 
+              {{tottickets.averagesolvingtime.hours}} hours,
+              {{tottickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          <td>n/a</td>
+          <td>
+            {% if lastmonthtickets.averagesolvingtime %}
+              {{lastmonthtickets.averagesolvingtime.days}} days, 
+              {{lastmonthtickets.averagesolvingtime.hours}} hours,
+              {{lastmonthtickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.averagesolvingtime > tottickets.averagesolvingtime %}
+                Up
+              {% elif lastmonthtickets.averagesolvingtime == tottickets.averagesolvingtime %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+      </tbody>
+    </table>
+
+    {% if categories %}
+        <h2>Prefered categories</h2>
+        <table>
+          <thead>
+            <tr>
+              <th>Category name</th>
+              <th>Number of projects</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for cat, count in categories %}
+              <tr>
+                <td><a href="/userstats/{{user.username}}/category/{{cat.fullname}}">{{cat.fullname}}</a></td>
+                <td>{{count}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+    {% endif %}
+    {% if category %}
+        <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+    {% else %}
+      <h2>Overall evaluation</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Field</th>
+            <th>Evaluation</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>Code contribution</td>
+            <td>
+               {% for i in range(codestars) %}★{% endfor %}
+               {% for i in range(5 - codestars) %}☆{% endfor %}
+            </td>
+          </tr>
+          <tr>
+            <td>Contribution to discussions on the forge</td>
+            <td>
+               {% for i in range(discussionstars) %}★{% endfor %}
+               {% for i in range(5 - discussionstars) %}☆{% endfor %}
+            </td>
+          </tr>
+          <tr>
+            <td>Contribution to issues solving</td>
+            <td>
+              {% for i in range(ticketsstars) %}★{% endfor %}
+              {% for i in range(5 - ticketsstars) %}☆{% endfor %}
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+  {% else %}
+    Invalid user!
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
new file mode 100644
index 0000000..148cfa8
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
@@ -0,0 +1,47 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Tickets
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Assigned tickets</th>
+            <th>Solved tickets</th>
+            <th>Revoked tickets</th>
+            <th>Average solving time</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.assigned}}</td>
+              <td>{{el.solved}}</td>
+              <td>{{el.revoked}}</td>
+              <td>
+                {% if el.averagesolvingtime %}
+                  {{el.averagesolvingtime.days}} days, 
+                  {{el.averagesolvingtime.hours}} hours,
+                  {{el.averagesolvingtime.minutes}} min
+                {% else %}n/a{% endif %}
+              </td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/artifacts.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/artifacts.html b/ForgeUserStats/forgeuserstats/templates/artifacts.html
new file mode 100644
index 0000000..9492628
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/artifacts.html
@@ -0,0 +1,52 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Artifacts
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+    <div class="grid-20">
+      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Created artifacts</th>
+            <th>Modified artifacts</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, row in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.created}}<br/>
+                {% endfor %}
+              </td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.modified}}<br/>
+                {% endfor %}
+              </td>
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/commits.html b/ForgeUserStats/forgeuserstats/templates/commits.html
new file mode 100644
index 0000000..2bca003
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/commits.html
@@ -0,0 +1,42 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Code contribution
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+    <div class="grid-20">
+      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Number of commits</th>
+            <th>Lines of code</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.number}}</td>
+              <td>{{el.lines}}</td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
new file mode 100644
index 0000000..502cb25
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -0,0 +1,423 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution
+    {% if category %}
+        in projects of category {{category.fullname}}
+    {% endif %}
+{% endblock %}
+
+{% block content %}
+  {% if user %}
+
+    <h2>General statistics</h2>
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Date</th>
+          <th>Time interval</th>
+        </tr>
+      </thead>
+      <tbody> 
+        <tr>
+          <td>Registration date</td>
+          <td>{{registration_date.strftime("%d %b %Y, %H:%M:%S (UTC)")}}</td> 
+           <td>{{days}} day{% if days != 1 %}s{% endif %} ago</td>
+        </tr>
+        {% if last_login %}
+          <tr>
+            <td>Last login</td>
+            <td>{{last_login.strftime("%d %b %Y, %H:%M:%S (UTC)")}}</td>
+            <td>{{last_login_days}} day{% if last_login_days != 1 %}s{% endif %} ago</td>
+          </tr>
+        {% endif %}
+      </tbody>
+    </table>
+
+    <h2>Contribution statistics</h2>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Total value</th>
+          <th>Average per-month value</th>
+          <th>Last 30 days</th>
+          {% if days >= 30 %}
+            <th>Trend</th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        {% if not category %}
+          <tr>
+            <td>Logins</td>
+            <td>{{totlogins}}</td>
+            <td>{{permonthlogins}}</td>
+            <td>{{lastmonth_logins}}</td>
+            {% if days >= 30 %}
+              <td style="text-align:center;">
+                {% if lastmonth_logins > permonthlogins %}
+                  <img src="{{g.forge_static('images/up.png')}}"/>
+                {% elif lastmonth_logins == permonthlogins %}
+                  <img src="{{g.forge_static('images/equal.png')}}"/>
+                {% else %}
+                  <img src="{{g.forge_static('images/down.png')}}"/>
+                {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endif %}
+        <tr>
+          <td>
+            {% if totcommits.number > 0 %}
+              <a href="/userstats/{{user.username}}/metric/commits/">Commits number</a>
+            {% else %}
+              Commits number
+            {% endif %}
+          </td>
+          <td>{{totcommits.number}}</td>
+          <td>{{permonthcommits.number}}</td>
+          <td>{{lastmonthcommits.number}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if permonthcommits.number > permonthcommits.number %}
+                  <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif permonthcommits.number == permonthcommits.number %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if totcommits.lines > 0 %}
+              <a href="/userstats/{{user.username}}/metric/commits/">Added/modified LOCs</a>
+            {% else %}
+              Added/modified LOCs
+            {% endif %}
+          </td>
+          <td>{{totcommits.lines}}</td>
+          <td>{{permonthcommits.lines}}</td>
+          <td>{{lastmonthcommits.lines}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if permonthcommits.lines > permonthcommits.lines %}
+                  <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif permonthcommits.lines == permonthcommits.lines %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if totartifacts.created > 0 %}
+              <a href="/userstats/{{user.username}}/metric/artifacts/">Total number of created artifacts</a>
+            {% else %}
+              Total number of created artifacts
+            {% endif %}
+          </td>
+          <td>{{totartifacts.created}}</td>
+          <td>{{permonthartifacts.created}}</td>
+          <td>{{lastmonthartifacts.created}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthartifacts.created > permonthartifacts.created %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthartifacts.created == permonthartifacts.created %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if totartifacts.modified > 0 %}
+              <a href="/userstats/{{user.username}}/metric/artifacts/">Total number of edited artifacts</a>
+            {% else %}
+              Total number of edited artifacts
+            {% endif %}
+          </td>
+          <td>{{totartifacts.modified}}</td>
+          <td>{{permonthartifacts.modified}}</td>
+          <td>{{lastmonthartifacts.modified}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthartifacts.modified > permonthartifacts.modified %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthartifacts.modified == permonthartifacts.modified %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        
+        {% for key, value in artifacts_by_type.items() %}
+          <tr>
+            <td>
+              {% if value.created > 0 %}
+                <a href="/userstats/{{user.username}}/metric/artifacts/">Created {{key}} artifacts</a>
+              {% else %}
+                Created {{key}} artifacts
+              {% endif %}
+            </td>
+            <td>{{value.created}}</td>
+            <td>{{value.pmcreated}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].created}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td style="text-align:center;">
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].created > value.pmcreated %}
+                    <img src="{{g.forge_static('images/up.png')}}"/>
+                  {% elif lastmonth_artifacts_by_type[key].created == value.pmcreated %}
+                    <img src="{{g.forge_static('images/equal.png')}}"/>
+                  {% else %}
+                    <img src="{{g.forge_static('images/down.png')}}"/>
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+          <tr>
+            <td>
+              {% if value.modified > 0 %}
+                <a href="/userstats/{{user.username}}/metric/artifacts/">Edited {{key}} artifacts</a>
+              {% else %}
+                Edited {{key}} artifacts
+              {% endif %}
+            </td>
+            <td>{{value.modified}}</td>
+            <td>{{value.pmmodified}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].modified}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td style="text-align:center;">
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].modified > value.pmmodified %}
+                    <img src="{{g.forge_static('images/up.png')}}"/>
+                  {% elif lastmonth_artifacts_by_type[key].modified == value.pmmodified %}
+                    <img src="{{g.forge_static('images/equal.png')}}"/>
+                  {% else %}
+                    <img src="{{g.forge_static('images/down.png')}}"/>
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endfor %}
+
+        <tr>
+          <td>
+            {% if tottickets.assigned > 0 %}
+              <a href="/userstats/{{user.username}}/metric/tickets/">Assigned tickets</a>
+            {% else %}
+              Assigned tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.assigned}}</td>
+          <td>{{permonthtickets.assigned}}</td>
+          <td>{{lastmonthtickets.assigned}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.assigned > permonthtickets.assigned %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.assigned == permonthtickets.assigned %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if tottickets.revoked > 0 %}
+              <a href="/userstats/{{user.username}}/metric/tickets/">Revoked tickets</a>
+            {% else %}
+              Revoked tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.revoked}}</td>
+          <td>{{permonthtickets.revoked}}</td>
+          <td>{{lastmonthtickets.revoked}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.revoked > permonthtickets.revoked %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.revoked == permonthtickets.revoked %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if tottickets.solved > 0 %}
+              <a href="/userstats/{{user.username}}/metric/tickets/">Solved tickets</a>
+            {% else %}
+              Solved tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.solved}}</td>
+          <td>{{permonthtickets.solved}}</td>
+          <td>{{lastmonthtickets.solved}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.solved > permonthtickets.solved %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.solved == permonthtickets.solved %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {% if tottickets.averagesolvingtime > 0 %}
+              <a href="/userstats/{{user.username}}/metric/tickets/">Average tickets solving time</a>
+            {% else %}
+              Average tickets solving time
+            {% endif %}
+          </td>
+          <td>
+            {% if tottickets.averagesolvingtime %}
+              {{tottickets.averagesolvingtime.days}} days, 
+              {{tottickets.averagesolvingtime.hours}} hours,
+              {{tottickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          <td>n/a</td>
+          <td>
+            {% if lastmonthtickets.averagesolvingtime %}
+              {{lastmonthtickets.averagesolvingtime.days}} days, 
+              {{lastmonthtickets.averagesolvingtime.hours}} hours,
+              {{lastmonthtickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.averagesolvingtime > tottickets.averagesolvingtime %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.averagesolvingtime == tottickets.averagesolvingtime %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+      </tbody>
+    </table>
+
+    {% if categories %}
+        <h2>Prefered categories</h2>
+        <p>
+          The following table shows the number projects tagged as belonging to each single category in which this user is involved.
+        </p>
+        <table>
+          <thead>
+            <tr>
+              <th>Category name</th>
+              <th>Number of projects</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for cat, count in categories %}
+              <tr>
+                <td><a href="/userstats/{{user.username}}/category/{{cat.fullname}}">{{cat.fullname}}</a></td>
+                <td>{{count}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+         
+        {% if categories|length > 1 %}
+          <p>
+            The same data listed in the previous table is graphically presented by the following histogram.
+          </p>
+          <p>
+            <img src="/userstats/{{user.username}}/categories_graph"/>
+          </p>
+        {% else %}
+          The following table shows the number projects tagged as belonging to each single category in which this user is involved.
+        {% endif %}
+    {% endif %}
+    {% if category %}
+        <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+    {% else %}
+      <h2>Overview</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Field</th>
+            <th>Value</th>
+            <th>Average per-user value</th>
+            <th>Maximum per-user value</th>
+            <th>Rank bar</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>Code</td>
+            <td>{{codecontribution}} LOC{% if codecontribution != 1 %}s{% endif %}/month</td>
+            <td>{{averagecodecontrib}} LOC{% if averagecodecontrib != 1 %}s{% endif %}/month</td>
+            <td>{{maxcodecontrib}} LOC{% if maxcodecontrib != 1 %}s{% endif %}/month</td>
+            <td><img src="/userstats/{{user.username}}/code_ranking_bar"/> {{codepercentage}} %</td>
+          </tr>
+          <tr>
+            <td>Discussion</td>
+            <td>{{discussioncontribution}} contr./month</td>
+            <td>{{averagedisccontrib}} contr./month</td>
+            <td>{{maxdisccontrib}} contr./month</td>
+            <td><img src="/userstats/{{user.username}}/discussion_ranking_bar"/> {{discussionpercentage}} %</td>
+          </tr>
+          <tr>
+            <td>Solved issues</td>
+            <td>{{ticketcontribution}} %</td>
+            <td>{{averageticketcontrib}} %</td>
+            <td>{{maxticketcontrib}} %</td>
+            <td><img src="/userstats/{{user.username}}/tickets_ranking_bar"/> {{ticketspercentage}} %</td>
+          </tr>
+        </tbody>
+      </table>
+      <h3>Note</h3>
+      <p>
+         The above table compares the average monthly contribution of this user with the average monthly contributions of the
+         other users of the forge. The progressbar and the percentage refer to the user's position in an overall ranking of the
+         users of this forge. For example, a value of 100% in the field "Code" is associated to the user who has the highest
+         average number of committed LOCs per month. Of course, this doesn't consider the quality of the contributions.
+      </p>
+    {% endif %}
+  {% else %}
+    Invalid user!
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/tickets.html b/ForgeUserStats/forgeuserstats/templates/tickets.html
new file mode 100644
index 0000000..9bf411b
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/tickets.html
@@ -0,0 +1,52 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Tickets
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+    <div class="grid-20">
+      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Assigned tickets</th>
+            <th>Solved tickets</th>
+            <th>Revoked tickets</th>
+            <th>Average solving time</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.assigned}}</td>
+              <td>{{el.solved}}</td>
+              <td>{{el.revoked}}</td>
+              <td>
+                {% if el.averagesolvingtime %}
+                  {{el.averagesolvingtime.days}} days, 
+                  {{el.averagesolvingtime.hours}} hours,
+                  {{el.averagesolvingtime.minutes}} min
+                {% else %}n/a{% endif %}
+              </td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/version.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/version.py b/ForgeUserStats/forgeuserstats/version.py
new file mode 100644
index 0000000..6514373
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/version.py
@@ -0,0 +1,2 @@
+__version_info__ = (0, 0)
+__version__ = '.'.join(map(str, __version_info__))

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/setup.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/setup.py b/ForgeUserStats/setup.py
new file mode 100644
index 0000000..dc2f07b
--- /dev/null
+++ b/ForgeUserStats/setup.py
@@ -0,0 +1,29 @@
+from setuptools import setup, find_packages
+import sys, os
+
+from forgeuserstats.version import __version__
+
+setup(name='ForgeUserStats',
+      version=__version__,
+      description="",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='',
+      author_email='',
+      url='',
+      license='',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+          'allura',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [allura.stats]
+      userstats=forgeuserstats.main:ForgeUserStatsApp
+      """,
+      )

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index 9621aa8..9ca2a92 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -47,6 +47,8 @@ TurboGears2==2.1.5
 WebOb==1.0.8
 # part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
 wsgiref==0.1.2
+numpy==1.6.1
+matplotlib==1.1.1rc
 
 # tg2 deps (not used directly)
 Babel==0.9.6


[47/48] git commit: [#5453] Moving userstats into a tool

Posted by tv...@apache.org.
[#5453] Moving userstats into a tool


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

Branch: refs/heads/si/5453
Commit: b0f9b70630602dc6146df96a601799b07a9f9cf8
Parents: e37eec8
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Feb 27 23:47:24 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py                  |   10 -
 Allura/allura/controllers/root.py                  |    4 +-
 .../ext/user_profile/templates/user_index.html     |    4 +-
 Allura/allura/ext/user_profile/user_main.py        |    9 +-
 Allura/allura/lib/app_globals.py                   |    2 +-
 Allura/allura/lib/plugin.py                        |    8 -
 Allura/allura/lib/widgets/forms.py                 |   15 -
 Allura/allura/model/project.py                     |    2 +
 Allura/allura/nf/allura/css/allura.css             |   13 +
 Allura/allura/templates/user_preferences.html      |    9 -
 .../forgeuserstats/controllers/userstats.py        |  235 +++++++++------
 ForgeUserStats/forgeuserstats/main.py              |   90 +++++-
 .../forgeuserstats/templates/artifacts.html        |    2 +-
 .../forgeuserstats/templates/commits.html          |    2 +-
 ForgeUserStats/forgeuserstats/templates/index.html |   32 +-
 .../forgeuserstats/templates/settings.html         |   19 ++
 .../forgeuserstats/templates/tickets.html          |    2 +-
 ForgeUserStats/forgeuserstats/widgets/forms.py     |   22 ++
 ForgeUserStats/setup.py                            |    6 +-
 19 files changed, 311 insertions(+), 175 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index a60b25d..f993ec4 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -58,7 +58,6 @@ class F(object):
     remove_inactive_period_form = forms.RemoveInactivePeriodForm()
     save_skill_form = forms.AddUserSkillForm()
     remove_skill_form = forms.RemoveSkillForm()
-    set_statistics = forms.StatsPreferencesForm()
 
 class AuthController(BaseController):
 
@@ -727,15 +726,6 @@ class SubscriptionsController(BaseController):
     @h.vardec
     @expose()
     @require_post()
-    @validate(F.set_statistics, error_handler=index)
-    def set_statistics(self, **kw):
-        require_authenticated()
-        c.user.stats.visible = kw.get('visible', True)
-        flash('Your preferences about statistics were successfully updated!')
-        redirect('.#Statistics')
-
-    @expose()
-    @require_post()
     def upload_sshkey(self, key=None):
         ap = plugin.AuthenticationProvider.get(request)
         try:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index a387b98..83aa5e5 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -69,9 +69,7 @@ class RootController(WsgiDispatchController):
         if n and not n.url_prefix.startswith('//'):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()
-        ep = g.entry_points["stats"].get('userstats')
-        if ep and g.show_userstats:
-            self.userstats = ep().root
+
         super(RootController, self).__init__()
 
     def _setup_request(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/ext/user_profile/templates/user_index.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/templates/user_index.html b/Allura/allura/ext/user_profile/templates/user_index.html
index 7371eb6..2b17b79 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -236,11 +236,11 @@
     </div>
   </div>
 
-  {% if statslinkurl %}
+  {% if user.stats.visible %}
     <div class="grid-24">
       <div class="grid-24" style="margin:0;"><b>User statistics</b></div>
       <div class="grid-24" style="margin-top:5px;margin-bottom:5px;">
-        <div><a href="{{statslinkurl}}"/>{{statslinkdescription}}</a></div>
+        <div><a href="{{c.project.url()}}userstats"/>Go to the personal statistics of this user</a></div>
       </div>
     </div>
   {% endif %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/ext/user_profile/user_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/user_main.py b/Allura/allura/ext/user_profile/user_main.py
index f3c3331..7754228 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -64,14 +64,7 @@ class UserProfileController(BaseController):
         user = c.project.user_project_of
         if not user:
             raise exc.HTTPNotFound()
-        if g.show_userstats and user.stats.visible:
-            from forgeuserstats.main import ForgeUserStatsApp
-            link, description = ForgeUserStatsApp.createlink(user)
-        else:
-            link, description = None, None
-        return dict(user=user,
-                    statslinkurl = link,
-                    statslinkdescription = description)
+        return dict(user=user)
     # This will be fully implemented in a future iteration
     # @expose('jinja:allura.ext.user_profile:templates/user_subscriptions.html')
     # def subscriptions(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 632a29f..ee2c9b9 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -177,7 +177,7 @@ class Globals(object):
         statslisteners = []
         for name, ep in self.entry_points['stats'].iteritems():
             if config.get('%s.enable' % name,'false')=='true':
-                statslisteners.append(ep().listener)
+                statslisteners.append(ep())
         self.statsUpdater = PostEvent(statslisteners)
 
     @LazyProperty

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 84e9f6e..2c8038f 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -738,14 +738,6 @@ class ThemeProvider(object):
         return RemoveInactivePeriodForm()
 
     @LazyProperty
-    def statistics_form(self):
-        '''
-        :return: None, or an easywidgets Form to render on the user preferences page
-        '''
-        from allura.lib.widgets.forms import StatsPreferencesForm
-        return StatsPreferencesForm(action='/auth/prefs/set_statistics')
-
-    @LazyProperty
     def add_trove_category(self):
         '''
         :return: None, or an easywidgets Form to render on the page to create a

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/lib/widgets/forms.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index c1a1d75..96f832d 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -452,21 +452,6 @@ class RemoveTimeSlotForm(ForgeForm):
         return d
 
 
-class StatsPreferencesForm(ForgeForm):
-    defaults=dict(ForgeForm.defaults)
-
-    class fields(ew_core.NameList):
-        visible = ew.Checkbox(
-            label='Make my personal statistics visible to other users.')
-            
-    def display(self, **kw):
-        if kw.get('user').stats.visible:
-            self.fields['visible'].attrs = {'checked':'true'}      
-        else:
-            self.fields['visible'].attrs = {}    
-        return super(ForgeForm, self).display(**kw)
-                
-
 class RemoveTroveCategoryForm(ForgeForm):
     defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 9f07f67..ab47f61 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -715,6 +715,8 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                         ('admin', 'admin', 'Admin'),
                         ('search', 'search', 'Search'),
                         ('activity', 'activity', 'Activity')]
+                if g.show_userstats:
+                    apps = apps + [('userstats', 'userstats', 'Statistics')]
             else:
                 apps = [('admin', 'admin', 'Admin'),
                         ('search', 'search', 'Search'),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/nf/allura/css/allura.css
----------------------------------------------------------------------
diff --git a/Allura/allura/nf/allura/css/allura.css b/Allura/allura/nf/allura/css/allura.css
index 1f32750..f3863b8 100644
--- a/Allura/allura/nf/allura/css/allura.css
+++ b/Allura/allura/nf/allura/css/allura.css
@@ -60,6 +60,11 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-repeat: no-repeat;
 }
 
+.ui-icon-tool-userstats {
+  background-image: url("../images/stats_24.png");
+  background-repeat: no-repeat;
+}
+
 .ui-icon-tool-admin, .ui-icon-admin {
   background-image: url("../images/admin_24.png");
   background-repeat: no-repeat;
@@ -118,6 +123,10 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 #top_nav .ui-icon-tool-stats {
   background-image: url("../images/stats_32.png");
 }
+#top_nav .ui-icon-tool-userstats {
+  background-image: url("../images/stats_32.png");
+}
+
 #top_nav .ui-icon-tool-admin, #top_nav .ui-icon-admin {
   background-image: url("../images/admin_32.png");
 }
@@ -152,6 +161,10 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 .big_icon.ui-icon-tool-stats {
   background-image: url("../images/stats_48.png");
 }
+.big_icon.ui-icon-tool-userstats {
+  background-image: url("../images/stats_48.png");
+}
+
 .big_icon.ui-icon-tool-admin, .big_icon.ui-icon-admin {
   background-image: url("../images/admin_48.png");
 }

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
index bd4c71e..768cd2e 100644
--- a/Allura/allura/templates/user_preferences.html
+++ b/Allura/allura/templates/user_preferences.html
@@ -129,15 +129,6 @@
     <ul><li><a href="/auth/prefs/user_skills">Click here to check and change your skills list</a></li></ul>
   </div>
 
-  {% if g.show_userstats %}
-    <a name="Statistics"></a>
-    <div class="grid-20">
-      <h2>Contribution statistics</h2>
-      <ul><li><a href="/userstats/{{c.user.username}}">Click here to check your personal statistics</a></li></ul>
-      {{g.theme.statistics_form.display(user=c.user)}}
-    </div>
-  {% endif %}
-
   {% if g.theme.password_change_form %}
   <div class="grid-20">
     <h2>Change Password</h2>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index 40ad92c..b93e77b 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -1,4 +1,4 @@
-from tg import expose
+from tg import expose, validate, redirect
 from tg.decorators import with_trailing_slash
 from datetime import datetime
 from allura.controllers import BaseController
@@ -6,33 +6,86 @@ import allura.model as M
 from allura.lib.graphics.graphic_methods import create_histogram, create_progress_bar
 from forgeuserstats.model.stats import UserStats
 from pylons import tmpl_context as c
+from allura.lib.security import require_access
+from forgeuserstats.widgets.forms import StatsPreferencesForm
+from allura.lib.decorators import require_post
+from allura.lib import validators as V
 
-class ForgeUserStatsController(BaseController):
+stats_preferences_form = StatsPreferencesForm()
+
+class ForgeUserStatsCatController(BaseController):
 
     @expose()
-    def _lookup(self, part, *remainder):
-        user = M.User.query.get(username=part)
+    def _lookup(self, category, *remainder):
+        cat = M.TroveCategory.query.get(shortname=category)
+        return ForgeUserStatsCatController(category = cat), remainder
 
-        if not self.user:
-            return ForgeUserStatsController(user=user), remainder
-        if part == "category":
-            return ForgeUserStatsCatController(self.user, None), remainder
-        if part == "metric":
-            return ForgeUserStatsMetricController(self.user), remainder
+    def __init__(self, category = None):
+        self.category = category
+        super(ForgeUserStatsCatController, self).__init__()
 
-    def __init__(self, user=None):
-        self.user = user
-        if self.user:
-            if not user.stats:
-                UserStats.create(self.user)
+    @expose('jinja:forgeuserstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return None
+        stats = self.user.stats
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
+        
+        cat_id = None
+        if self.category: 
+            cat_id = self.category._id
+        ret_dict = _getDataForCategory(cat_id, stats)
+        ret_dict['user'] = self.user
+        ret_dict['registration_date'] = stats.registration_date
+        ret_dict['category'] = self.category
+        return ret_dict
 
-        super(ForgeUserStatsController, self).__init__()
+class ForgeUserStatsController(BaseController):
+
+    category = ForgeUserStatsCatController()            
+    
+    @expose('jinja:forgeuserstats:templates/settings.html')
+    @with_trailing_slash
+    def settings(self, **kw):
+        require_access(c.project, 'admin')
+
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+        return dict(
+            user = self.user, 
+            form = StatsPreferencesForm(
+                action = c.project.url() + 'userstats/change_settings'))
+      
+    @expose()
+    @require_post()
+    @validate(stats_preferences_form, error_handler=settings)
+    def change_settings(self, **kw):
+        require_access(c.project, 'admin')
+
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+        visible = kw.get('visible')
+        self.user.stats.visible = visible
+        redirect(c.project.url() + 'userstats/settings')
 
     @expose('jinja:forgeuserstats:templates/index.html')
     @with_trailing_slash
     def index(self, **kw):
+        self.user = c.project.user_project_of
         if not self.user: 
             return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+
         stats = self.user.stats
         if (not stats.visible) and (c.user != self.user):
             return dict(user=self.user)
@@ -79,11 +132,71 @@ class ForgeUserStatsController(BaseController):
             stats.getMaxAndAverageDiscussionContribution()
         ret_dict['maxticketcontrib'], ret_dict['averageticketcontrib'] =\
             stats.getMaxAndAverageTicketsSolvingPercentage()
-
+        
         return ret_dict
 
+
+    @expose('jinja:forgeuserstats:templates/commits.html')
+    @with_trailing_slash
+    def commits(self, **kw):
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+        stats = self.user.stats
+
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
+        
+        commits = stats.getCommitsByCategory()
+        return dict(
+            user = self.user,
+            data = commits) 
+
+    @expose('jinja:forgeuserstats:templates/artifacts.html')
+    @with_trailing_slash
+    def artifacts(self, **kw):
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+        stats = self.user.stats
+
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
+
+        stats = self.user.stats       
+        artifacts = stats.getArtifactsByCategory(detailed=True)
+        return dict(
+            user = self.user,
+            data = artifacts)
+
+    @expose('jinja:forgeuserstats:templates/tickets.html')
+    @with_trailing_slash
+    def tickets(self, **kw):
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return dict(user=None)
+        if not self.user.stats:
+            UserStats.create(self.user)
+        stats = self.user.stats
+
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
+
+        artifacts = self.user.stats.getTicketsByCategory()
+        return dict(
+            user=self.user,
+            data=artifacts)
+
     @expose()
     def categories_graph(self):
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return None
+
         categories = {}
         for p in self.user.my_projects():
             for cat in p.trove_topic:
@@ -109,89 +222,27 @@ class ForgeUserStatsController(BaseController):
 
     @expose()
     def code_ranking_bar(self):
-        return create_progress_bar(self.user.stats.codeRanking())
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return None
+        stats = self.user.stats
+        return create_progress_bar(stats.codeRanking())
 
     @expose()
     def discussion_ranking_bar(self):
-        return create_progress_bar(self.user.stats.discussionRanking())
+        self.user = c.project.user_project_of
+        if not self.user: 
+            return None
+        stats = self.user.stats
+        return create_progress_bar(stats.discussionRanking())
 
     @expose()
     def tickets_ranking_bar(self):
-        return create_progress_bar(self.user.stats.ticketsRanking())
-
-class ForgeUserStatsCatController(BaseController):
-    @expose()
-    def _lookup(self, category, *remainder):
-        cat = M.TroveCategory.query.get(shortname=category)
-        return ForgeUserStatsCatController(self.user, cat), remainder
-
-    def __init__(self, user, category):
-        self.user = user
-        self.category = category
-        super(ForgeUserStatsCatController, self).__init__()
-
-    @expose('jinja:forgeuserstats:templates/index.html')
-    @with_trailing_slash
-    def index(self, **kw):
-        if not self.user:
-            return dict(user=None)
-        stats = self.user.stats
-        if (not stats.visible) and (c.user != self.user):
-            return dict(user=self.user)
-        
-        cat_id = None
-        if self.category: 
-            cat_id = self.category._id
-        ret_dict = _getDataForCategory(cat_id, stats)
-        ret_dict['user'] = self.user
-        ret_dict['registration_date'] = stats.registration_date
-        ret_dict['category'] = self.category
-        
-        return ret_dict
-
-class ForgeUserStatsMetricController(BaseController):
-
-    def __init__(self, user):
-        self.user = user
-        super(ForgeUserStatsMetricController, self).__init__()
-
-    @expose('jinja:forgeuserstats:templates/commits.html')
-    @with_trailing_slash
-    def commits(self, **kw):
-        if not self.user:
-            return dict(user=None)
-        stats = self.user.stats
-        if (not stats.visible) and (c.user != self.user):
-            return dict(user=self.user)
-        
-        commits = stats.getCommitsByCategory()
-        return dict(user = self.user,
-                    data = commits) 
-
-    @expose('jinja:forgeuserstats:templates/artifacts.html')
-    @with_trailing_slash
-    def artifacts(self, **kw):
-        if not self.user:
-            return dict(user=None)
-        stats = self.user.stats
-        if (not stats.visible) and (c.user != self.user):
-            return dict(user=self.user)
-
-        stats = self.user.stats       
-        artifacts = stats.getArtifactsByCategory(detailed=True)
-        return dict(user = self.user, data = artifacts) 
-
-    @expose('jinja:forgeuserstats:templates/tickets.html')
-    @with_trailing_slash
-    def tickets(self, **kw):
+        self.user = c.project.user_project_of
         if not self.user: 
-            return dict(user=None)
+            return None
         stats = self.user.stats
-        if (not stats.visible) and (c.user != self.user):
-            return dict(user=self.user)
-
-        artifacts = self.user.stats.getTicketsByCategory()
-        return dict(user = self.user, data = artifacts) 
+        return create_progress_bar(stats.ticketsRanking())
 
 def _getDataForCategory(category, stats):
     totcommits = stats.getCommits(category)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
index 8eeb113..dcecdb9 100644
--- a/ForgeUserStats/forgeuserstats/main.py
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -1,10 +1,24 @@
+#-*- python -*-
 import logging
+from pylons import tmpl_context as c
+import formencode
+from formencode import validators
+from webob import exc
 from datetime import datetime
 
+from allura.app import Application, SitemapEntry
+from allura.lib import helpers as h
+from allura.lib.security import has_access
+from allura import model as M
 from allura.eventslistener import EventsListener
 from model.stats import UserStats
 from controllers.userstats import ForgeUserStatsController
 
+from forgeuserstats import version
+from forgeuserstats.controllers.userstats import ForgeUserStatsController
+
+from ming.orm import session
+
 log = logging.getLogger(__name__)
 
 class UserStatsListener(EventsListener):
@@ -55,12 +69,74 @@ class UserStatsListener(EventsListener):
     def newOrganization(self, organization):
         pass
 
-class ForgeUserStatsApp:
+class ForgeUserStatsApp(Application):
+    __version__ = version.__version__
+    tool_label='Statistics'
+    default_mount_label='Statistics'
+    default_mount_point='stats'
+    permissions = ['configure', 'read', 'write',
+                    'unmoderated_post', 'post', 'moderate', 'admin']
+    ordinal=15
+    installable=False
+    config_options = Application.config_options
+    default_external_feeds = []
+    icons={
+        24:'images/stats_24.png',
+        32:'images/stats_32.png',
+        48:'images/stats_48.png'
+    }
     root = ForgeUserStatsController()
-    listener = UserStatsListener()
 
-    @classmethod
-    def createlink(cls, user):
-        return (
-            "/userstats/%s/" % user.username, 
-            "%s personal statistcs" % user.display_name)
+    def __init__(self, project, config):
+        Application.__init__(self, project, config)
+        role_admin = M.ProjectRole.by_name('Admin')._id
+        role_anon = M.ProjectRole.by_name('*anonymous')._id
+        self.config.acl = [
+            M.ACE.allow(role_anon, 'read'),
+            M.ACE.allow(role_admin, 'admin')]
+
+    def main_menu(self):
+        return [SitemapEntry(self.config.options.mount_label.title(), '.')]
+
+    @property
+    @h.exceptionless([], log)
+    def sitemap(self):
+        menu_id = self.config.options.mount_label.title()
+        with h.push_config(c, app=self):
+            return [
+                SitemapEntry(menu_id, '.')[self.sidebar_menu()] ]
+
+    @property
+    def show_discussion(self):
+        if 'show_discussion' in self.config.options:
+            return self.config.options['show_discussion']
+        else:
+            return True
+
+    @h.exceptionless([], log)
+    def sidebar_menu(self):
+        base = c.app.url
+        links = [SitemapEntry('Overview', base),
+                 SitemapEntry('Commits', base + 'commits'),
+                 SitemapEntry('Artifacts', base + 'artifacts'),
+                 SitemapEntry('Tickets', base + 'tickets')]
+        return links
+
+    def admin_menu(self):
+        links = [SitemapEntry(
+                     'Settings', c.project.url() + 'userstats/settings')]
+        return links
+
+    def install(self, project):
+        #It doesn't make any sense to install the tool twice on the same 
+        #project therefore, if it already exists, it doesn't install it
+        #a second time.
+        for tool in project.app_configs:
+            if tool.tool_name == 'userstats':
+                if self.config.options.mount_point!=tool.options.mount_point:
+                    project.uninstall_app(self.config.options.mount_point)
+                    return
+
+    def uninstall(self, project):
+        self.config.delete()
+        session(self.config).flush()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/templates/artifacts.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/artifacts.html b/ForgeUserStats/forgeuserstats/templates/artifacts.html
index 013c108..ede6dde 100644
--- a/ForgeUserStats/forgeuserstats/templates/artifacts.html
+++ b/ForgeUserStats/forgeuserstats/templates/artifacts.html
@@ -11,7 +11,7 @@
 
   {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
-      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+      <ul><li><a href="{{c.project.url()}}userstats">Go back to general statistics</a></li></ul>
     </div>
 
     {% if data %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/commits.html b/ForgeUserStats/forgeuserstats/templates/commits.html
index 10d1c67..f0aca6f 100644
--- a/ForgeUserStats/forgeuserstats/templates/commits.html
+++ b/ForgeUserStats/forgeuserstats/templates/commits.html
@@ -11,7 +11,7 @@
 
   {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
-      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+      <ul><li><a href="{{c.project.url()}}userstats">Go back to general statistics</a></li></ul>
     </div>
 
     {% if data %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index 653cd31..ae58cf8 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -15,7 +15,7 @@
 
     {% if category %}
        <ul>
-         <li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li>
+         <li><a href="{{c.project.url()}}userstats">Go back to general statistics</a></li>
        </ul>
     {% endif %}
     <h2>General statistics</h2>
@@ -80,7 +80,7 @@
         <tr>
           <td>
             {% if totcommits.number > 0 %}
-              <a href="/userstats/{{user.username}}/metric/commits/">Commits number</a>
+              <a href="{{c.project.url()}}userstats/commits/">Commits number</a>
             {% else %}
               Commits number
             {% endif %}
@@ -103,7 +103,7 @@
         <tr>
           <td>
             {% if totcommits.lines > 0 %}
-              <a href="/userstats/{{user.username}}/metric/commits/">Added/modified LOCs</a>
+              <a href="{{c.project.url()}}userstats/commits/">Added/modified LOCs</a>
             {% else %}
               Added/modified LOCs
             {% endif %}
@@ -126,7 +126,7 @@
         <tr>
           <td>
             {% if totartifacts.created > 0 %}
-              <a href="/userstats/{{user.username}}/metric/artifacts/">Total number of created artifacts</a>
+              <a href="{{c.project.url()}}userstats/artifacts/">Total number of created artifacts</a>
             {% else %}
               Total number of created artifacts
             {% endif %}
@@ -149,7 +149,7 @@
         <tr>
           <td>
             {% if totartifacts.modified > 0 %}
-              <a href="/userstats/{{user.username}}/metric/artifacts/">Total number of edited artifacts</a>
+              <a href="{{c.project.url()}}userstats/artifacts/">Total number of edited artifacts</a>
             {% else %}
               Total number of edited artifacts
             {% endif %}
@@ -174,7 +174,7 @@
           <tr>
             <td>
               {% if value.created > 0 %}
-                <a href="/userstats/{{user.username}}/metric/artifacts/">Created {{key}} artifacts</a>
+                <a href="{{c.project.url()}}userstats/artifacts/">Created {{key}} artifacts</a>
               {% else %}
                 Created {{key}} artifacts
               {% endif %}
@@ -205,7 +205,7 @@
           <tr>
             <td>
               {% if value.modified > 0 %}
-                <a href="/userstats/{{user.username}}/metric/artifacts/">Edited {{key}} artifacts</a>
+                <a href="{{c.project.url()}}userstats/artifacts/">Edited {{key}} artifacts</a>
               {% else %}
                 Edited {{key}} artifacts
               {% endif %}
@@ -238,7 +238,7 @@
         <tr>
           <td>
             {% if tottickets.assigned > 0 %}
-              <a href="/userstats/{{user.username}}/metric/tickets/">Assigned tickets</a>
+              <a href="{{c.project.url()}}userstats/tickets/">Assigned tickets</a>
             {% else %}
               Assigned tickets
             {% endif %}
@@ -261,7 +261,7 @@
         <tr>
           <td>
             {% if tottickets.revoked > 0 %}
-              <a href="/userstats/{{user.username}}/metric/tickets/">Revoked tickets</a>
+              <a href="{{c.project.url()}}userstats/tickets/">Revoked tickets</a>
             {% else %}
               Revoked tickets
             {% endif %}
@@ -284,7 +284,7 @@
         <tr>
           <td>
             {% if tottickets.solved > 0 %}
-              <a href="/userstats/{{user.username}}/metric/tickets/">Solved tickets</a>
+              <a href="{{c.project.url()}}userstats/tickets/">Solved tickets</a>
             {% else %}
               Solved tickets
             {% endif %}
@@ -307,7 +307,7 @@
         <tr>
           <td>
             {% if tottickets.averagesolvingtime > 0 %}
-              <a href="/userstats/{{user.username}}/metric/tickets/">Average tickets solving time</a>
+              <a href="{{c.project.url()}}userstats/tickets/">Average tickets solving time</a>
             {% else %}
               Average tickets solving time
             {% endif %}
@@ -357,7 +357,7 @@
           <tbody>
             {% for cat, count in categories %}
               <tr>
-                <td><a href="/userstats/{{user.username}}/category/{{cat.shortname}}">{{cat.fullname}}</a></td>
+                <td><a href="{{c.project.url()}}userstats/category/{{cat.shortname}}">{{cat.fullname}}</a></td>
                 <td>{{count}}</td>
               </tr>
             {% endfor %}
@@ -369,7 +369,7 @@
             The same data listed in the previous table is graphically presented by the following histogram.
           </p>
           <p>
-            <img src="/userstats/{{user.username}}/categories_graph"/>
+            <img src="{{c.project.url()}}userstats/categories_graph"/>
           </p>
         {% endif %}
     {% endif %}
@@ -391,21 +391,21 @@
             <td>{{codecontribution}} LOC{% if codecontribution != 1 %}s{% endif %}/month</td>
             <td>{{averagecodecontrib}} LOC{% if averagecodecontrib != 1 %}s{% endif %}/month</td>
             <td>{{maxcodecontrib}} LOC{% if maxcodecontrib != 1 %}s{% endif %}/month</td>
-            <td><img src="/userstats/{{user.username}}/code_ranking_bar"/> {{codepercentage}} %</td>
+            <td><img src="{{c.project.url()}}userstats/code_ranking_bar"/> {{codepercentage}} %</td>
           </tr>
           <tr>
             <td>Discussion</td>
             <td>{{discussioncontribution}} contr./month</td>
             <td>{{averagedisccontrib}} contr./month</td>
             <td>{{maxdisccontrib}} contr./month</td>
-            <td><img src="/userstats/{{user.username}}/discussion_ranking_bar"/> {{discussionpercentage}} %</td>
+            <td><img src="{{c.project.url()}}userstats/discussion_ranking_bar"/> {{discussionpercentage}} %</td>
           </tr>
           <tr>
             <td>Solved issues</td>
             <td>{{ticketcontribution}} %</td>
             <td>{{averageticketcontrib}} %</td>
             <td>{{maxticketcontrib}} %</td>
-            <td><img src="/userstats/{{user.username}}/tickets_ranking_bar"/> {{ticketspercentage}} %</td>
+            <td><img src="{{c.project.url()}}userstats/tickets_ranking_bar"/> {{ticketspercentage}} %</td>
           </tr>
         </tbody>
       </table>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/templates/settings.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/settings.html b/ForgeUserStats/forgeuserstats/templates/settings.html
new file mode 100644
index 0000000..c07301a
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/settings.html
@@ -0,0 +1,19 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats – Settings{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Settings
+{% endblock %}
+
+{% block content %}
+
+    <div class="grid-20">
+      In this page you can set the visibility of your personal statistics. If you decide to hide your personal statistics to 
+      other users, data collected about your contributions to projects hosted on this forge will be available only for your
+      personal use. 
+      {{form.display(user = user)}}
+    </div>
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/tickets.html b/ForgeUserStats/forgeuserstats/templates/tickets.html
index a713b25..0252021 100644
--- a/ForgeUserStats/forgeuserstats/templates/tickets.html
+++ b/ForgeUserStats/forgeuserstats/templates/tickets.html
@@ -11,7 +11,7 @@
 
   {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
-      <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
+      <ul><li><a href="{{c.project.url()}}userstats">Go back to general statistics</a></li></ul>
     </div>
 
     {% if data %}

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/forgeuserstats/widgets/forms.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/widgets/forms.py b/ForgeUserStats/forgeuserstats/widgets/forms.py
new file mode 100644
index 0000000..c4da4fb
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/widgets/forms.py
@@ -0,0 +1,22 @@
+from allura.lib import validators as V
+from allura.lib.widgets.forms import ForgeForm
+
+from formencode import validators as fev
+
+import ew as ew_core
+import ew.jinja2_ew as ew
+
+class StatsPreferencesForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        visible = ew.Checkbox(
+            label='Make my personal statistics visible to other users.')
+            
+    def display(self, **kw):
+        if kw.get('user').stats.visible:
+            self.fields['visible'].attrs = {'checked':'true'}      
+        else:
+            self.fields['visible'].attrs = {}    
+        return super(ForgeForm, self).display(**kw)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b0f9b706/ForgeUserStats/setup.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/setup.py b/ForgeUserStats/setup.py
index dc2f07b..733ebd8 100644
--- a/ForgeUserStats/setup.py
+++ b/ForgeUserStats/setup.py
@@ -23,7 +23,11 @@ setup(name='ForgeUserStats',
       ],
       entry_points="""
       # -*- Entry points: -*-
-      [allura.stats]
+      [allura]
       userstats=forgeuserstats.main:ForgeUserStatsApp
+
+      [allura.stats]
+      userstats=forgeuserstats.main:UserStatsListener
+
       """,
       )


[02/48] git commit: Merge branch 'db/5755'

Posted by tv...@apache.org.
Merge branch 'db/5755'


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

Branch: refs/heads/si/5453
Commit: 1b4ce947c1d34c7ef4a36e185d7a76ba57ab51e7
Parents: ea380e8 234ef74
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 12 20:42:32 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Mar 12 20:42:32 2013 +0000

----------------------------------------------------------------------
 .../allura/ext/admin/templates/project_admin.html  |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------



[28/48] git commit: [5453] Removed .svn

Posted by tv...@apache.org.
[5453] Removed .svn


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

Branch: refs/heads/si/5453
Commit: f94f0d48ec5bab19c69b3e556b1a61e7c7cd9ac9
Parents: 794ae63
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 18 18:20:56 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 .../forgeuserstats/model/.svn/all-wcprops          |   17 -
 ForgeUserStats/forgeuserstats/model/.svn/entries   |   96 ---
 .../model/.svn/text-base/stats.py.svn-base         |  534 ---------------
 .../forgeuserstats/templates/.svn/all-wcprops      |   29 -
 .../forgeuserstats/templates/.svn/entries          |  164 -----
 .../.svn/text-base/artifacts.html.svn-base         |   48 --
 .../templates/.svn/text-base/commits.html.svn-base |   37 -
 .../templates/.svn/text-base/index.html.svn-base   |  341 ---------
 .../templates/.svn/text-base/tickets.html.svn-base |   47 --
 9 files changed, 0 insertions(+), 1313 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops b/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
deleted file mode 100644
index a5d5661..0000000
--- a/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
+++ /dev/null
@@ -1,17 +0,0 @@
-K 25
-svn:wc:ra_dav:version-url
-V 58
-/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model
-END
-stats.py
-K 25
-svn:wc:ra_dav:version-url
-V 67
-/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model/stats.py
-END
-__init__.py
-K 25
-svn:wc:ra_dav:version-url
-V 70
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/model/__init__.py
-END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/model/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/entries b/ForgeUserStats/forgeuserstats/model/.svn/entries
deleted file mode 100644
index c26dfd9..0000000
--- a/ForgeUserStats/forgeuserstats/model/.svn/entries
+++ /dev/null
@@ -1,96 +0,0 @@
-10
-
-dir
-4
-https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats/model
-https://xp-dev.com/svn/allura
-
-
-
-2012-10-19T08:28:36.749162Z
-3
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-46ed536d-f66c-413e-a53e-834384f708db
-
-stats.py
-file
-
-
-
-
-2012-11-05T14:43:25.729756Z
-21591047edf4fabfb1b70150af5bd0c2
-2012-10-19T08:28:36.749162Z
-3
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-23647
-
-__init__.py
-file
-
-
-
-
-2012-11-05T14:43:25.729756Z
-d41d8cd98f00b204e9800998ecf8427e
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-0
-

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base b/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base
deleted file mode 100644
index e69de29..0000000

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base b/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
deleted file mode 100644
index f434e4e..0000000
--- a/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
+++ /dev/null
@@ -1,534 +0,0 @@
-import pymongo
-from pylons import c, g, request
-
-import bson
-from ming import schema as S
-from ming import Field, Index, collection
-from ming.orm import session, state, Mapper
-from ming.orm import FieldProperty, RelationProperty, ForeignIdProperty
-from ming.orm.declarative import MappedClass
-from datetime import datetime, timedelta
-import difflib
-
-from allura.model.session import main_orm_session, main_doc_session
-from allura.model.session import project_orm_session
-from allura.model import User
-import allura.model as M
-from allura.lib import helpers as h
-
-class UserStats(MappedClass):
-    SALT_LEN=8
-    class __mongometa__:
-        name='userstats'
-        session = main_orm_session
-        unique_indexes = [ 'userid' ]
-
-    _id=FieldProperty(S.ObjectId)
-    userid = ForeignIdProperty('User')
-
-    registration_date = FieldProperty(datetime)
-    tot_logins_count = FieldProperty(int, if_missing = 0)
-    last_login = FieldProperty(datetime)
-    general = FieldProperty([dict(category = S.ObjectId,
-                                  messages = [dict(messagetype = str,
-                                                   created = int,
-                                                   modified = int)],
-                                  tickets = dict(solved = int,
-                                                 assigned = int,
-                                                 revoked = int,
-                                                 totsolvingtime = int),
-                                  commits = [dict(lines = int,
-                                                  number = int,
-                                                  language = S.ObjectId)])])
-
-    lastmonth= FieldProperty(dict(logins=[datetime],
-                                  messages=[dict(datetime=datetime,
-                                                 created=bool,
-                                                 categories=[S.ObjectId],
-                                                 messagetype=str)],
-                                  assignedtickets=[dict(datetime=datetime,
-                                                        categories=[S.ObjectId])],
-                                  revokedtickets=[dict(datetime=datetime,
-                                                       categories=[S.ObjectId])],
-                                  solvedtickets=[dict(datetime=datetime,
-                                                      categories=[S.ObjectId],
-                                                      solvingtime=int)],
-                                  commits=[dict(datetime=datetime,
-                                                categories=[S.ObjectId],
-                                                programming_languages=[S.ObjectId],
-                                                lines=int)]))
-    reluser = RelationProperty('User')
-
-
-    def codeRanking(self) :
-        def _getCodeContribution(stats) :
-            for val in stats['general'] :
-                if val['category'] is None :
-                    for commits in val['commits'] :
-                         if commits['language'] is None : 
-                             return (commits.lines, commits.number)
-            return (0,0) 
-
-        lst = list(self.query.find())
-        totn = len(lst)
-        codcontr = _getCodeContribution(self)
-        upper = len([x for x in lst if _getCodeContribution(x) > codcontr])
-        percentage = upper * 100.0 / totn
-        if percentage < 1 / 6.0 : return 5
-        if percentage < 2 / 6.0 : return 4
-        if percentage < 3 / 6.0 : return 3
-        if percentage < 4 / 6.0 : return 2
-        if percentage < 5 / 6.0 : return 1
-        return 0
-
-    def discussionRanking(self) :
-        def _getDiscussionContribution(stats) :
-            for val in stats['general'] :
-                if val['category'] is None :
-                    for artifact in val['messages'] :
-                         if artifact['messagetype'] is None : 
-                             return artifact.created + artifact.modified
-            return 0
-
-        lst = list(self.query.find())
-        totn = len(lst)
-        disccontr = _getDiscussionContribution(self)
-        upper = len([x for x in lst if _getDiscussionContribution(x) > disccontr])
-        percentage = upper * 100.0 / totn
-        if percentage < 1 / 6.0 : return 5
-        if percentage < 2 / 6.0 : return 4
-        if percentage < 3 / 6.0 : return 3
-        if percentage < 4 / 6.0 : return 2
-        if percentage < 5 / 6.0 : return 1
-        return 0
-
-    def ticketsRanking(self) :
-
-        def _getTicketsPercentage(stats) :
-            for val in stats['general'] :
-                if val['category'] is None :
-                    if val['tickets']['assigned'] == 0 : percentage = 0
-                    else :
-                        percentage = val['tickets']['solved'] \
-                                    / val['tickets']['assigned']
-            return 0
-
-        percentage = _getTicketsPercentage(self)
-        if percentage > 1 / 6.0 : return 5
-        if percentage > 2 / 6.0 : return 4
-        if percentage > 3 / 6.0 : return 3
-        if percentage > 4 / 6.0 : return 2
-        if percentage > 5 / 6.0 : return 1
-        return 0
-
-    def getCommits(self, category = None) :
-        i = getElementIndex(self.general, category = category)
-        if i is None : return {'number' : 0, 'lines': 0}
-        cat = self.general[i]
-        j = getElementIndex(cat.commits, language = None)
-        if j is None : return {'number' : 0, 'lines': 0}
-        return {'number': cat.commits[j]['number'], 
-                'lines' : cat.commits[j]['lines']}
-
-    def getArtifacts(self, category = None, art_type = None) :
-        i = getElementIndex(self.general, category = category)
-        if i is None : return {'created' : 0, 'modified': 0}
-        cat = self.general[i]
-        j = getElementIndex(cat.messages, art_type = art_type)
-        if j is None : return {'created' : 0, 'modified': 0}
-        return {'created'  : cat[j].created, 
-                'modified' : cat[j].modified}
-
-    def getTickets(self, category = None) :
-        i = getElementIndex(self.general, category = category)
-        if i is None : return {'assigned'           : 0,
-                               'solved'             : 0,
-                               'revoked'            : 0,
-                               'averagesolvingtime' : None}
-        if self.general[i].tickets.solved > 0 :
-           tot = self.general[i].tickets.totsolvingtime 
-           number = self.general[i].tickets.solved
-           average = tot / number
-
-        else : average = None
-        return {'assigned'           : self.general[i].tickets.assigned,
-                'solved'             : self.general[i].tickets.solved,
-                'revoked'            : self.general[i].tickets.revoked,
-                'averagesolvingtime' : _convertTimeDiff(average)}
-
-    def getCommitsByCategory(self) :
-        by_cat = {}
-        for entry in self.general :
-            cat = entry.category
-            i = getElementIndex(entry.commits, language = None)
-            if i is None : n, lines = 0, 0
-            else : n, lines = entry.commits[i].number, entry.commits[i].lines
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            by_cat[cat] = {'number' : n, 'lines' : lines}
-        return by_cat
-
-    def getCommitsByLanguage(self) :
-        langlist = []
-        by_lang = {}
-        i = getElementIndex(self.general, category=None)
-        if i is None : return {'number' : 0, 'lines' : 0}
-        return dict([(el.language, {'lines' : el.lines, 'number':el.number})
-                     for el in self.general[i].commits])
-
-    def getArtifactsByCategory(self, detailed=False) :
-        by_cat = {}
-        for entry in self.general :
-            cat = entry.category
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            if detailed : 
-                by_cat[cat] = entry.messages
-            else : 
-                i = getElementIndex(entry.messages, messagetype=None)
-                if i is not None : by_cat[cat] = entry.messages[i]
-                else : by_cat[cat] = {'created' : 0, 'modified' : 0}
-        return by_cat
-
-    def getArtifactsByType(self, category=None) :
-        i = getElementIndex(self.general, category = category)
-        if i is None : return {}
-        entry = self.general[i].messages
-        by_type = dict([(el.messagetype, {'created' : el.created, 
-                                          'modified': el.modified})
-                         for el in entry])
-        return by_type
-
-    def getTicketsByCategory(self) :
-        by_cat = {}
-        for entry in self.general :
-            cat = entry.category
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            a, s = entry.tickets.assigned, entry.tickets.solved
-            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
-            if s : average = time / s
-            else : average = None
-            by_cat[cat] = {'assigned'           : a,
-                           'solved'             : s,
-                           'revoked'            : r, 
-                           'averagesolvingtime' : _convertTimeDiff(average)}
-        return by_cat
-
-    def getLastMonthCommits(self, category = None) :
-        self.checkOldArtifacts() 
-        lineslist = [el.lines for el in self.lastmonth.commits
-                     if category in el.categories + [None]]
-        return {'number': len(lineslist), 'lines':sum(lineslist)}
-
-    def getLastMonthCommitsByCategory(self) :
-        self.checkOldArtifacts() 
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-
-        by_cat = {}
-        for cat in catlist :
-            lineslist = [el.lines for el in self.lastmonth.commits
-                         if cat in el.categories + [None]]
-            n = len(lineslist)
-            lines = sum(lineslist)
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            by_cat[cat] = {'number' : n, 'lines' : lines}
-        return by_cat
-
-    def getLastMonthCommitsByLanguage(self) :
-        self.checkOldArtifacts() 
-        seen = set()
-        langlist=[el.language for el in self.general
-                  if el.language not in seen and not seen.add(el.language)]
-
-        by_lang = {}
-        for lang in langlist :
-            lineslist = [el.lines for el in self.lastmonth.commits
-                         if lang in el.programming_languages + [None]]
-            n = len(lineslist)
-            lines = sum(lineslist)
-            if lang != None : lang = M.TroveCategory.query.get(_id = lang)
-            by_lang[lang] = {'number' : n, 'lines' : lines}
-        return by_lang
-
-    def getLastMonthArtifacts(self, category = None) :
-        self.checkOldArtifacts() 
-        cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
-                                     for el in self.lastmonth.messages
-                                     if category is None or 
-                                        category in el.categories], (0,0))
-        return {'created': cre, 'modified' : mod}
-
-    def getLastMonthArtifactsByType(self, category = None) :
-        self.checkOldArtifacts() 
-        seen = set()
-        types=[el.messagetype for el in self.lastmonth.messages
-               if el.messagetype not in seen and not seen.add(el.messagetype)]
-
-        by_type = {}
-        for t in types :
-            cre, mod = reduce(addtuple, 
-                              [(int(el.created),1-int(el.created))
-                               for el in self.lastmonth.messages
-                               if el.messagetype == t and
-                                  category in [None]+el.categories],
-                              (0,0))
-            by_type[t] = {'created': cre, 'modified' : mod}
-        return by_type
-
-    def getLastMonthArtifactsByCategory(self) :
-        self.checkOldArtifacts() 
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-
-        by_cat = {}
-        for cat in catlist :
-            cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
-                                         for el in self.lastmonth.messages 
-                                         if cat in el.categories + [None]], (0,0))
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            by_cat[cat] = {'created' : cre, 'modified' : mod}
-        return by_cat
-
-    def getLastMonthTickets(self, category = None) :
-        self.checkOldArtifacts()
-        a = len([el for el in self.lastmonth.assignedtickets
-                 if category in el.categories + [None]])
-        r = len([el for el in self.lastmonth.revokedtickets
-                 if category in el.categories + [None]])
-        s, time = reduce(addtuple, 
-                         [(1, el.solvingtime)
-                          for el in self.lastmonth.solvedtickets
-                          if category in el.categories + [None]],
-                         (0,0))
-        if category!=None : category = M.TroveCategory.query.get(_id=category)
-        if s > 0 : time = time / s
-        else : time = None
-        return {'assigned'           : a,
-                'revoked'            : r,
-                'solved'             : s, 
-                'averagesolvingtime' : _convertTimeDiff(time)}
-        
-    def getLastMonthTicketsByCategory(self) :
-        self.checkOldArtifacts()
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-        by_cat = {}
-        for cat in catlist :
-            a = len([el for el in self.lastmonth.assignedtickets
-                     if cat in el.categories + [None]])
-            r = len([el for el in self.lastmonth.revokedtickets
-                     if cat in el.categories + [None]])
-            s, time = reduce(addtuple, [(1, el.solvingtime)
-                                        for el in self.lastmonth.solvedtickets
-                                        if cat in el.categories + [None]],(0,0))
-            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
-            if s > 0 : time = time / s
-            else : time = None
-            by_cat[cat] = {'assigned'           : a,
-                           'revoked'            : r,
-                           'solved'             : s, 
-                           'averagesolvingtime' : _convertTimeDiff(time)}
-        return by_cat
-        
-    def getLastMonthLogins(self) :
-        self.checkOldArtifacts()
-        return len(self.lastmonth.logins)
-
-    def checkOldArtifacts(self) :
-        now = datetime.now()
-        for m in self.lastmonth.messages :
-            if now - m.datetime > timedelta(30) :
-               self.lastmonth.messages.remove(m)
-        for t in self.lastmonth.assignedtickets :
-            if now - t.datetime > timedelta(30) :
-               self.lastmonth.assignedtickets.remove(t)
-        for t in self.lastmonth.revokedtickets :
-            if now - t.datetime > timedelta(30) :
-               self.lastmonth.revokedtickets.remove(t)
-        for t in self.lastmonth.solvedtickets :
-            if now - t.datetime > timedelta(30) :
-               self.lastmonth.solvedtickets.remove(t)
-
-    def addNewArtifact(self, art_type, art_datetime, project) :
-        self._updateArtifactsStats(art_type, art_datetime, project, "created")
-
-    def addModifiedArtifact(self, art_type, art_datetime, project) :
-        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
-
-    def addAssignedTicket(self, ticket, project) :
-        topics = [t for t in project.trove_topic if t]
-        self._updateTicketsStats(topics, 'assigned')
-        self.lastmonth.assignedtickets.append({'datetime'   : ticket.mod_date,
-                                               'categories' : topics})
-
-    def addRevokedTicket(self, ticket, project) :
-        topics = [t for t in project.trove_topic if t]
-        self._updateTicketsStats(topics, 'revoked')
-        self.lastmonth.revokedtickets.append({'datetime'   : ticket.mod_date,
-                                              'categories' : topics})
-        self.checkOldArtifacts()
-
-    def addClosedTicket(self, ticket, project) :
-        topics = [t for t in project.trove_topic if t]
-        s_time=int((datetime.utcnow()-ticket.created_date).total_seconds())
-        self._updateTicketsStats(topics, 'solved', s_time = s_time)
-        self.lastmonth.solvedtickets.append({'datetime'   : ticket.mod_date,
-                                             'categories' : topics,
-                                             'solvingtime': s_time})
-        self.checkOldArtifacts()
-
-    def addCommit(self, newcommit, project) :
-        def _addCommitData(stats, topics, languages, newblob, oldblob = None) :
-            if oldblob : listold = list(oldblob)
-            else : listold = []
-            listnew = list(newblob)
-
-            if oldblob is None : lines = len(listnew)
-            elif newblob.has_html_view :
-                diff = difflib.unified_diff(listold, listnew,
-                         ('old' + oldblob.path()).encode('utf-8'),
-                         ('new' + newblob.path()).encode('utf-8'))
-                lines = len([l for l in diff if len(l) > 0 and l[0] == '+']) - 1
-            else : lines = 0
-            
-            lt = topics + [None]
-            ll = languages + [None]
-            for t in lt :
-                i = getElementIndex(stats.general, category=t) 
-                if i is None :
-                    newstats = {'category' : t,
-                                'commits'  : [],
-                                'tickets'  : {'assigned'       : 0,
-                                              'solved'         : 0,
-                                              'revoked'        : 0,
-                                              'totsolvingtime' : 0},
-                                'messages' : []}   
-                    stats.general.append(newstats)
-                    i = getElementIndex(stats.general, category=t)
-                for lang in ll :
-                    j = getElementIndex(stats.general[i]['commits'], 
-                                        language=lang)
-                    if j is None :
-                        stats.general[i]['commits'].append({'language': lang,
-                                                            'lines'   : lines,
-                                                            'number'  : 1})
-                    else :
-                        stats.general[i]['commits'][j].lines += lines
-                        stats.general[i]['commits'][j].number += 1
-            return lines
-
-        topics = [t for t in project.trove_topic if t]
-        languages = [l for l in project.trove_language if l]
-        now = datetime.utcnow()
-
-        d = newcommit.diffs
-        if len(newcommit.parent_ids) > 0 :
-            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
-
-        totlines = 0
-        for changed in d.changed :
-            newblob = newcommit.tree.get_blob_by_path(changed)
-            oldblob = oldcommit.tree.get_blob_by_path(changed)
-            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
-
-        for copied in d.copied :
-            newblob = newcommit.tree.get_blob_by_path(copied['new'])
-            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
-            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
-
-        for added in d.added :
-            newblob = newcommit.tree.get_blob_by_path(added)
-            totlines+=_addCommitData(self, topics, languages, newblob)
-
-        self.lastmonth.commits.append({'datetime' : now,
-                                       'categories' : topics,
-                                       'programming_languages' : languages,
-                                       'lines' : totlines})
-        self.checkOldArtifacts()
-
-    def addLogin(self) :
-        now = datetime.utcnow()
-        self.last_login = now
-        self.tot_logins_count += 1
-        self.lastmonth.logins.append(now)
-        self.checkOldArtifacts()
-        
-    def _updateArtifactsStats(self, art_type, art_datetime, project, action) :
-        if action not in ['created', 'modified'] : return
-        topics = [t for t in project.trove_topic if t]
-        lt = [None] + topics
-        for mtype in [None, art_type] :
-            for t in lt :
-                i = getElementIndex(self.general, category = t)
-                if i is None :
-                    msg = {'category' : t,
-                           'commits'  : [],
-                           'tickets'  : {'solved'         : 0,
-                                         'assigned'       : 0,
-                                         'revoked'        : 0,
-                                         'totsolvingtime' : 0},
-                           'messages' : []}
-                    self.general.append(msg)
-                    i = getElementIndex(self.general, category = t)
-                j = getElementIndex(self.general[i]['messages'], messagetype = mtype)
-                if j is None : 
-                    entry = {'messagetype' : mtype,
-                             'created'     : 0,
-                             'modified'    : 0}
-                    entry[action] += 1
-                    self.general[i]['messages'].append(entry)
-                else : self.general[i]['messages'][j][action] += 1
-
-        self.lastmonth.messages.append({'datetime'   : art_datetime,
-                                        'created'    : action == 'created',
-                                        'categories' : topics,
-                                        'messagetype': art_type})
-        self.checkOldArtifacts() 
-
-    def _updateTicketsStats(self, topics, action, s_time = None) :
-        if action not in ['solved', 'assigned', 'revoked'] : return
-        lt = topics + [None]
-        for t in lt :
-            i = getElementIndex(self.general, category = t)
-            if i is None :
-                stats = {'category' : t,
-                         'commits'  : [],
-                         'tickets'  : {'solved'         : 0,
-                                       'assigned'       : 0,
-                                       'revoked'        : 0,
-                                       'totsolvingtime' : 0},
-                         'messages' : [] }
-                self.general.append(stats)
-                i = getElementIndex(self.general, category = t)
-            self.general[i]['tickets'][action] += 1 
-            if action == 'solved' : 
-                self.general[i]['tickets']['totsolvingtime']+=s_time
-
-def getElementIndex(el_list, **kw) :
-    for i in range(len(el_list)) :
-        for k in kw : 
-            if el_list[i].get(k) != kw[k] : break
-        else : return i
-    return None
-
-def addtuple(l1, l2) :
-    a, b = l1
-    x, y = l2
-    return (a+x, b+y)
-
-def _convertTimeDiff(int_seconds) :
-    if int_seconds is None : return None
-    diff = timedelta(seconds = int_seconds)
-    days, seconds = diff.days, diff.seconds
-    hours = seconds / 3600
-    seconds = seconds % 3600
-    minutes = seconds / 60
-    seconds = seconds % 60
-    return {'days'    : days, 
-            'hours'   : hours, 
-            'minutes' : minutes,
-            'seconds' : seconds}
-
-Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops b/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
deleted file mode 100644
index efae2aa..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
+++ /dev/null
@@ -1,29 +0,0 @@
-K 25
-svn:wc:ra_dav:version-url
-V 62
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates
-END
-commits.html
-K 25
-svn:wc:ra_dav:version-url
-V 75
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/commits.html
-END
-artifacts.html
-K 25
-svn:wc:ra_dav:version-url
-V 77
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/artifacts.html
-END
-tickets.html
-K 25
-svn:wc:ra_dav:version-url
-V 75
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/tickets.html
-END
-index.html
-K 25
-svn:wc:ra_dav:version-url
-V 73
-/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/index.html
-END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/entries b/ForgeUserStats/forgeuserstats/templates/.svn/entries
deleted file mode 100644
index ef7dfdb..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/entries
+++ /dev/null
@@ -1,164 +0,0 @@
-10
-
-dir
-4
-https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats/templates
-https://xp-dev.com/svn/allura
-
-
-
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-46ed536d-f66c-413e-a53e-834384f708db
-
-tickets.html
-file
-
-
-
-
-2012-11-05T14:43:25.725756Z
-4bac229c573965dbfd312e65cc7313a2
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-1361
-
-index.html
-file
-
-
-
-
-2012-11-05T14:43:25.725756Z
-036136344f0b3099f212c6c749431996
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-11126
-
-commits.html
-file
-
-
-
-
-2012-11-05T14:43:25.725756Z
-cbfcdaeb670c8896e31071077c51eb23
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-955
-
-artifacts.html
-file
-
-
-
-
-2012-11-05T14:43:25.725756Z
-bb6c7ceabf56de25d177ee5cd52451ab
-2012-10-17T19:55:53.450112Z
-1
-stefanoinvernizzi
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-1386
-

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
deleted file mode 100644
index 0b3cfb8..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/artifacts.html.svn-base
+++ /dev/null
@@ -1,48 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}User stats{% endblock %}
-
-{% block header %}
-    Statistics about {{user.display_name}}'s contribution – Artifacts
-{% endblock %}
-
-{% block content %}
-
-  {% if user %}
-
-    {% if data %}
-      <h2>Statistics by category</h2>
-      <table>
-        <thead>
-          <tr>
-            <th>Category</th>
-            <th>Created artifacts</th>
-            <th>Modified artifacts</th>
-          </tr>
-        </thead>
-        <tbody> 
-          {% for cat, row in data.items() %}
-            <tr>
-              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
-              <td>
-                {% for details in row %}
-                  {% if details.messagetype %} {{details.messagetype}}:
-                  {% else %}Total:{% endif %} {{details.created}}<br/>
-                {% endfor %}
-              </td>
-              <td>
-                {% for details in row %}
-                  {% if details.messagetype %} {{details.messagetype}}:
-                  {% else %}Total:{% endif %} {{details.modified}}<br/>
-                {% endfor %}
-              </td>
-            </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-    {% endif %}
-    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
-  {% endif %}
-
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
deleted file mode 100644
index c574c9f..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/commits.html.svn-base
+++ /dev/null
@@ -1,37 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}User stats{% endblock %}
-
-{% block header %}
-    Statistics about {{user.display_name}}'s contribution – Code contribution
-{% endblock %}
-
-{% block content %}
-
-  {% if user %}
-
-    {% if data %}
-      <h2>Statistics by category</h2>
-      <table>
-        <thead>
-          <tr>
-            <th>Category</th>
-            <th>Number of commits</th>
-            <th>Lines of code</th>
-          </tr>
-        </thead>
-        <tbody> 
-          {% for cat, el in data.items() %}
-            <tr>
-              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
-              <td>{{el.number}}</td>
-              <td>{{el.lines}}</td>
-            {% endfor %}
-          </tr>
-        </tbody>
-      </table>
-    {% endif %}
-    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
-  {% endif %}
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
deleted file mode 100644
index b53e596..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/index.html.svn-base
+++ /dev/null
@@ -1,341 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}User stats{% endblock %}
-
-{% block header %}
-    Statistics about {{user.display_name}}'s contribution
-    {% if category %}
-        in projects of category {{category.fullname}}
-    {% endif %}
-{% endblock %}
-
-{% block content %}
-  {% if user %}
-
-    <h2>General statistics</h2>
-    <table>
-      <thead>
-        <tr>
-          <th>Parameter</th>
-          <th>Value</th>
-        </tr>
-      </thead>
-      <tbody> 
-        <tr>
-          <td>Registration date</td>
-          <td>
-            {{registration_date.strftime("%d %b %Y, %H:%M:%S (UTC)")}}, 
-            {{days}} day{% if days != 1 %}s{% endif %} ago</td>
-        </tr>
-        {% if last_login %}
-          <tr>
-            <td>Last login</td>
-            <td>
-              {{last_login.strftime("%d %b %Y, %H:%M:%S (UTC)")}},
-              {{last_login_days}} day{% if last_login_days != 1 %}s{% endif %} ago</td>
-            </td>
-          </tr>
-        {% endif %}
-      </tbody>
-    </table>
-
-    <h2>Contribution statistics</h2>
-
-    <table>
-      <thead>
-        <tr>
-          <th>Parameter</th>
-          <th>Total value</th>
-          <th>Average per-month value</th>
-          <th>Last 30 days</th>
-          {% if days >= 30 %}
-            <th>Trend</th>
-          {% endif %}
-        </tr>
-      </thead>
-      <tbody>
-        {% if not category %}
-          <tr>
-            <td>Logins</td>
-            <td>{{totlogins}}</td>
-            <td>{{permonthlogins}}</td>
-            <td>{{lastmonth_logins}}</td>
-            {% if days >= 30 %}
-              <td>
-                {% if lastmonth_logins > permonthlogins %}
-                  Up
-                {% elif lastmonth_logins == permonthlogins %}
-                  =
-                {% else %}
-                  Down
-                {%endif%}
-              </td>
-            {% endif %}
-          </tr>
-        {% endif %}
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/commits/">Commits number</a></td>
-          <td>{{totcommits.number}}</td>
-          <td>{{permonthcommits.number}}</td>
-          <td>{{lastmonthcommits.number}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if permonthcommits.number > permonthcommits.number %}
-                Up
-              {% elif permonthcommits.number == permonthcommits.number %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/commits/">Added/modified LOCs</a></td>
-          <td>{{totcommits.lines}}</td>
-          <td>{{permonthcommits.lines}}</td>
-          <td>{{lastmonthcommits.lines}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if permonthcommits.lines > permonthcommits.lines %}
-                Up
-              {% elif permonthcommits.lines == permonthcommits.lines %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/artifacts/">Total number of created artifacts</a></td>
-          <td>{{totartifacts.created}}</td>
-          <td>{{permonthartifacts.created}}</td>
-          <td>{{lastmonthartifacts.created}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthartifacts.created > permonthartifacts.created %}
-                Up
-              {% elif lastmonthartifacts.created == permonthartifacts.created %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/commits/">Total number of edited artifacts</a></td>
-          <td>{{totartifacts.modified}}</td>
-          <td>{{permonthartifacts.modified}}</td>
-          <td>{{lastmonthartifacts.modified}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthartifacts.modified > permonthartifacts.modified %}
-                Up
-              {% elif lastmonthartifacts.modified == permonthartifacts.modified %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        
-        {% for key, value in artifacts_by_type.items() %}
-          <tr>
-            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Created {{key}} artifacts</a></td>
-            <td>{{value.created}}</td>
-            <td>{{value.pmcreated}}</td>
-            <td>
-              {% if lastmonth_artifacts_by_type.get(key) %}
-                 {{lastmonth_artifacts_by_type[key].created}}
-              {% else %}
-                 0
-              {% endif %}
-            </td>
-            {% if days >= 30 %}
-              <td>
-                {% if lastmonth_artifacts_by_type.get(key) %}
-                  {% if lastmonth_artifacts_by_type[key].created > value.pmcreated %}
-                    Up
-                  {% elif lastmonth_artifacts_by_type[key].created == value.pmcreated %}
-                    =
-                  {% else %}
-                    Down
-                  {%endif%}
-                {%else%} Down {%endif%}
-              </td>
-            {% endif %}
-          </tr>
-          <tr>
-            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Edited {{key}} artifacts</a></td>
-            <td>{{value.modified}}</td>
-            <td>{{value.pmmodified}}</td>
-            <td>
-              {% if lastmonth_artifacts_by_type.get(key) %}
-                 {{lastmonth_artifacts_by_type[key].modified}}
-              {% else %}
-                 0
-              {% endif %}
-            </td>
-            {% if days >= 30 %}
-              <td>
-                {% if lastmonth_artifacts_by_type.get(key) %}
-                  {% if lastmonth_artifacts_by_type[key].modified > value.pmmodified %}
-                    Up
-                  {% elif lastmonth_artifacts_by_type[key].modified == value.pmmodified %}
-                    =
-                  {% else %}
-                    Down
-                  {%endif%}
-                {%else%} Down {%endif%}
-              </td>
-            {% endif %}
-          </tr>
-        {% endfor %}
-
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/tickets/">Assigned tickets</a></td>
-          <td>{{tottickets.assigned}}</td>
-          <td>{{permonthtickets.assigned}}</td>
-          <td>{{lastmonthtickets.assigned}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthtickets.assigned > permonthtickets.assigned %}
-                Up
-              {% elif lastmonthtickets.assigned == permonthtickets.assigned %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/tickets/">Revoked tickets</a></td>
-          <td>{{tottickets.revoked}}</td>
-          <td>{{permonthtickets.revoked}}</td>
-          <td>{{lastmonthtickets.revoked}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthtickets.revoked > permonthtickets.revoked %}
-                Up
-              {% elif lastmonthtickets.revoked == permonthtickets.revoked %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/tickets/">Solved tickets</a></td>
-          <td>{{tottickets.solved}}</td>
-          <td>{{permonthtickets.solved}}</td>
-          <td>{{lastmonthtickets.solved}}</td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthtickets.solved > permonthtickets.solved %}
-                Up
-              {% elif lastmonthtickets.solved == permonthtickets.solved %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-        <tr>
-          <td><a href="/userstats/{{user.username}}/metric/tickets/">Average tickets solving time</a></td>
-          <td>
-            {% if tottickets.averagesolvingtime %}
-              {{tottickets.averagesolvingtime.days}} days, 
-              {{tottickets.averagesolvingtime.hours}} hours,
-              {{tottickets.averagesolvingtime.minutes}} min
-            {% else %}n/a{% endif %}
-          </td>
-          <td>n/a</td>
-          <td>
-            {% if lastmonthtickets.averagesolvingtime %}
-              {{lastmonthtickets.averagesolvingtime.days}} days, 
-              {{lastmonthtickets.averagesolvingtime.hours}} hours,
-              {{lastmonthtickets.averagesolvingtime.minutes}} min
-            {% else %}n/a{% endif %}
-          </td>
-          {% if days >= 30 %}
-            <td>
-              {% if lastmonthtickets.averagesolvingtime > tottickets.averagesolvingtime %}
-                Up
-              {% elif lastmonthtickets.averagesolvingtime == tottickets.averagesolvingtime %}
-                =
-              {% else %}
-                Down
-              {%endif%}
-            </td>
-          {% endif %}
-        </tr>
-      </tbody>
-    </table>
-
-    {% if categories %}
-        <h2>Prefered categories</h2>
-        <table>
-          <thead>
-            <tr>
-              <th>Category name</th>
-              <th>Number of projects</th>
-            </tr>
-          </thead>
-          <tbody>
-            {% for cat, count in categories %}
-              <tr>
-                <td><a href="/userstats/{{user.username}}/category/{{cat.fullname}}">{{cat.fullname}}</a></td>
-                <td>{{count}}</td>
-              </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-    {% endif %}
-    {% if category %}
-        <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
-    {% else %}
-      <h2>Overall evaluation</h2>
-      <table>
-        <thead>
-          <tr>
-            <th>Field</th>
-            <th>Evaluation</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr>
-            <td>Code contribution</td>
-            <td>
-               {% for i in range(codestars) %}★{% endfor %}
-               {% for i in range(5 - codestars) %}☆{% endfor %}
-            </td>
-          </tr>
-          <tr>
-            <td>Contribution to discussions on the forge</td>
-            <td>
-               {% for i in range(discussionstars) %}★{% endfor %}
-               {% for i in range(5 - discussionstars) %}☆{% endfor %}
-            </td>
-          </tr>
-          <tr>
-            <td>Contribution to issues solving</td>
-            <td>
-              {% for i in range(ticketsstars) %}★{% endfor %}
-              {% for i in range(5 - ticketsstars) %}☆{% endfor %}
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    {% endif %}
-  {% else %}
-    Invalid user!
-  {% endif %}
-
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f94f0d48/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base b/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
deleted file mode 100644
index 148cfa8..0000000
--- a/ForgeUserStats/forgeuserstats/templates/.svn/text-base/tickets.html.svn-base
+++ /dev/null
@@ -1,47 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}User stats{% endblock %}
-
-{% block header %}
-    Statistics about {{user.display_name}}'s contribution – Tickets
-{% endblock %}
-
-{% block content %}
-
-  {% if user %}
-
-    {% if data %}
-      <h2>Statistics by category</h2>
-      <table>
-        <thead>
-          <tr>
-            <th>Category</th>
-            <th>Assigned tickets</th>
-            <th>Solved tickets</th>
-            <th>Revoked tickets</th>
-            <th>Average solving time</th>
-          </tr>
-        </thead>
-        <tbody> 
-          {% for cat, el in data.items() %}
-            <tr>
-              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
-              <td>{{el.assigned}}</td>
-              <td>{{el.solved}}</td>
-              <td>{{el.revoked}}</td>
-              <td>
-                {% if el.averagesolvingtime %}
-                  {{el.averagesolvingtime.days}} days, 
-                  {{el.averagesolvingtime.hours}} hours,
-                  {{el.averagesolvingtime.minutes}} min
-                {% else %}n/a{% endif %}
-              </td>
-            {% endfor %}
-          </tr>
-        </tbody>
-      </table>
-    {% endif %}
-    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
-  {% endif %}
-{% endblock %}


[42/48] git commit: [#5453] Fix initialization of the stats object

Posted by tv...@apache.org.
[#5453] Fix initialization of the stats object


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

Branch: refs/heads/si/5453
Commit: 5d10f4443776f4104653e915ad67be289d548203
Parents: 6e8b937
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Mar 6 22:39:25 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py |    6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/5d10f444/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index adc36db..51121ad 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -498,12 +498,12 @@ class Stats(MappedClass):
                     newstats = dict(
                         category=t,
                         commits=[],
-                        messages=dict(
+                        messages=[dict(
                             assigned=0,
                             solved=0,
                             revoked=0,
-                            totsolvingtime=0),
-                        tickets=[])   
+                            totsolvingtime=0)],
+                        tickets={})
                     stats.general.append(newstats)
                     i = getElementIndex(stats.general, category=t)
                 for lang in ll:


[32/48] git commit: [5453] Removed wrong message in userstats Web page

Posted by tv...@apache.org.
[5453] Removed wrong message in userstats Web page


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

Branch: refs/heads/si/5453
Commit: 4c2fdbe22046e85f2a77ebc07ec99959612d0768
Parents: 17275bb
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 15:10:59 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/templates/index.html |    2 --
 1 files changed, 0 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4c2fdbe2/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index 502cb25..f5a71c6 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -366,8 +366,6 @@
           <p>
             <img src="/userstats/{{user.username}}/categories_graph"/>
           </p>
-        {% else %}
-          The following table shows the number projects tagged as belonging to each single category in which this user is involved.
         {% endif %}
     {% endif %}
     {% if category %}


[25/48] git commit: [5453] Removed loops through entry points

Posted by tv...@apache.org.
[5453] Removed loops through entry points


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

Branch: refs/heads/si/5453
Commit: 5369e11525cdba590f575dc4d3abc8c7a11f9543
Parents: caf94e1
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 19 17:29:34 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/root.py |    6 +++---
 Allura/allura/model/auth.py       |   21 +++++++++------------
 2 files changed, 12 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/5369e115/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 859394b..a387b98 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -69,9 +69,9 @@ class RootController(WsgiDispatchController):
         if n and not n.url_prefix.startswith('//'):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()
-        for ep in pkg_resources.iter_entry_points("allura.stats"):
-            if ep.name.lower() == 'userstats' and g.show_userstats:
-                setattr(self, ep.name.lower(), ep.load()().root)
+        ep = g.entry_points["stats"].get('userstats')
+        if ep and g.show_userstats:
+            self.userstats = ep().root
         super(RootController, self).__init__()
 
     def _setup_request(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/5369e115/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 9ed7067..23f5366 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -36,13 +36,6 @@ from .timeline import ActivityNode, ActivityObject
 
 log = logging.getLogger(__name__)
 
-#This is just to keep the UserStats module completely optional
-has_user_stats_module = False
-for ep in iter_entry_points("allura.stats"):
-    if ep.name.lower() == 'userstats':
-        from forgeuserstats.model.stats import UserStats
-        has_user_stats_module = True
-
 def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
     """
     Returns a bytestring version of 's', encoded as specified in 'encoding'.
@@ -340,16 +333,20 @@ class User(MappedClass, ActivityNode, ActivityObject):
         comment=str)])
 
     #Statistics
-    if has_user_stats_module:
-        stats_id = ForeignIdProperty('UserStats', if_missing=None)
-        stats = RelationProperty('UserStats', via='stats_id')
-    else:
-        stats_id = FieldProperty(S.ObjectId, if_missing=None)
+    stats_id = FieldProperty(S.ObjectId, if_missing=None)
 
     @property
     def activity_name(self):
         return self.display_name or self.username
 
+    @property
+    def stats(self):
+        if g.show_userstats:
+            from forgeuserstats.model.stats import UserStats
+            return UserStats.query.get(_id=self.stats_id)
+        else: 
+            return None
+
     def get_pref(self, pref_name):
         return plugin.UserPreferencesProvider.get().get_pref(self, pref_name)
 


[17/48] git commit: [5453] replace datetime.now with datetime.utcnow

Posted by tv...@apache.org.
[5453] replace datetime.now with datetime.utcnow


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

Branch: refs/heads/si/5453
Commit: 35f76dda72391e0326fac056c49b3768bcb4345e
Parents: 0df516e
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 10 15:41:14 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/35f76dda/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 6e9d063..c622888 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -431,7 +431,7 @@ class UserStats(MappedClass):
         return len(self.lastmonth.logins)
 
     def checkOldArtifacts(self):
-        now = datetime.now()
+        now = datetime.utcnow()
         for m in self.lastmonth.messages:
             if now - m.datetime > timedelta(30):
                 self.lastmonth.messages.remove(m)


[04/48] git commit: [#5909] Split Preferences and Personal Info off from Subscriptions page

Posted by tv...@apache.org.
[#5909] Split Preferences and Personal Info off from Subscriptions page

Signed-off-by: Cory Johns <jo...@geek.net>


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

Branch: refs/heads/si/5453
Commit: b55760a66e263adf4626ee8ff146e6a1b19fb149
Parents: 8feb541
Author: Cory Johns <jo...@geek.net>
Authored: Wed Mar 13 14:56:46 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:46 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py                  |  309 +++++++++------
 Allura/allura/lib/plugin.py                        |   28 +-
 Allura/allura/nf/allura/css/site_style.css         |   77 ++++
 .../templates/jinja_master/theme_macros.html       |    4 +-
 Allura/allura/templates/oauth_authorize_ok.html    |    4 +-
 Allura/allura/templates/trovecategories.html       |    2 +-
 Allura/allura/templates/user_availability.html     |   45 ++-
 Allura/allura/templates/user_contacts.html         |   35 +-
 Allura/allura/templates/user_info.html             |   47 +++
 Allura/allura/templates/user_preferences.html      |  195 ---------
 Allura/allura/templates/user_prefs.html            |  140 +++++++
 Allura/allura/templates/user_skills.html           |   33 +-
 Allura/allura/templates/user_subs.html             |   54 +++
 Allura/allura/tests/functional/test_auth.py        |  100 +++---
 14 files changed, 632 insertions(+), 441 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 462f449..e52ea5e 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -62,7 +62,9 @@ class F(object):
 class AuthController(BaseController):
 
     def __init__(self):
-        self.prefs = PreferencesController()
+        self.preferences = PreferencesController()
+        self.user_info = UserInfoController()
+        self.subscriptions = SubscriptionsController()
         self.oauth = OAuthController()
 
     @expose('jinja:allura:templates/login.html')
@@ -188,7 +190,7 @@ class AuthController(BaseController):
         if c.user:
             c.user.claim_openid(oid_obj._id)
             flash('Claimed %s' % oid_obj._id)
-        redirect('/auth/prefs/')
+        redirect('/auth/preferences/')
 
     @expose()
     def logout(self):
@@ -293,21 +295,169 @@ class AuthController(BaseController):
                     allow_write=has_access(c.app, 'write')(user=user),
                     allow_create=has_access(c.app, 'create')(user=user))
 
+class PreferencesController(BaseController):
+
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/user_prefs.html')
+    def index(self, **kw):
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        api_token = M.ApiToken.query.get(user_id=c.user._id)
+        return dict(
+                menu=menu,
+                api_token=api_token,
+                authorized_applications=M.OAuthAccessToken.for_user(c.user),
+            )
+
+    @h.vardec
+    @expose()
+    @require_post()
+    def update(self,
+               addr=None,
+               new_addr=None,
+               primary_addr=None,
+               oid=None,
+               new_oid=None,
+               preferences=None,
+               **kw):
+        if config.get('auth.method', 'local') == 'local':
+            if not preferences.get('display_name'):
+                flash("Display Name cannot be empty.",'error')
+                redirect('.')
+            c.user.set_pref('display_name', preferences['display_name'])
+            for i, (old_a, data) in enumerate(zip(c.user.email_addresses, addr or [])):
+                obj = c.user.address_object(old_a)
+                if data.get('delete') or not obj:
+                    del c.user.email_addresses[i]
+                    if obj: obj.delete()
+            c.user.set_pref('email_address', primary_addr)
+            if new_addr.get('claim'):
+                if M.EmailAddress.query.get(_id=new_addr['addr'], confirmed=True):
+                    flash('Email address already claimed', 'error')
+                else:
+                    c.user.email_addresses.append(new_addr['addr'])
+                    em = M.EmailAddress.upsert(new_addr['addr'])
+                    em.claimed_by_user_id=c.user._id
+                    em.send_verification_link()
+            for i, (old_oid, data) in enumerate(zip(c.user.open_ids, oid or [])):
+                obj = c.user.openid_object(old_oid)
+                if data.get('delete') or not obj:
+                    del c.user.open_ids[i]
+                    if obj: obj.delete()
+            for k,v in preferences.iteritems():
+                if k == 'results_per_page':
+                    v = int(v)
+                c.user.set_pref(k, v)
+        redirect('.')
+
+    @expose()
+    @require_post()
+    def gen_api_token(self):
+        tok = M.ApiToken.query.get(user_id=c.user._id)
+        if tok is None:
+            tok = M.ApiToken(user_id=c.user._id)
+        else:
+            tok.secret_key = h.cryptographic_nonce()
+        redirect(request.referer)
+
+    @expose()
+    @require_post()
+    def del_api_token(self):
+        tok = M.ApiToken.query.get(user_id=c.user._id)
+        if tok is None: return
+        tok.delete()
+        redirect(request.referer)
+
+    @expose()
+    @require_post()
+    def revoke_oauth(self, _id=None):
+        tok = M.OAuthAccessToken.query.get(_id=bson.ObjectId(_id))
+        if tok is None:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        if tok.user_id != c.user._id:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        tok.delete()
+        flash('Application access revoked')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def change_password(self, **kw):
+        kw = g.theme.password_change_form.to_python(kw, None)
+        ap = plugin.AuthenticationProvider.get(request)
+        try:
+            ap.set_password(c.user, kw['oldpw'], kw['pw'])
+        except wexc.HTTPUnauthorized:
+            flash('Incorrect password', 'error')
+            redirect('.')
+        flash('Password changed')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    def upload_sshkey(self, key=None):
+        ap = plugin.AuthenticationProvider.get(request)
+        try:
+            ap.upload_sshkey(c.user.username, key)
+        except AssertionError, ae:
+            flash('Error uploading key: %s' % ae, 'error')
+        flash('Key uploaded')
+        redirect('.')
+
+class UserInfoController(BaseController):
+
+    def __init__(self, *args, **kwargs):
+        self.skills = UserSkillsController()
+        self.contacts = UserContactsController()
+        self.availability = UserAvailabilityController()
+
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/user_info.html')
+    def index(self, **kw):
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
+
+    @expose()
+    @require_post()
+    @validate(F.change_personal_data_form, error_handler=index)
+    def change_personal_data(self, **kw):
+        require_authenticated()
+        c.user.set_pref('sex', kw['sex'])
+        c.user.set_pref('birthdate', kw.get('birthdate'))
+        localization={'country':kw.get('country'), 'city':kw.get('city')}
+        c.user.set_pref('localization', localization)
+        c.user.set_pref('timezone', kw['timezone'])
+
+        flash('Your personal data was successfully updated!')
+        redirect('.')
+
 class UserSkillsController(BaseController):
 
     def __init__(self, category=None):
         self.category = category
         super(UserSkillsController, self).__init__()
 
+    def _check_security(self):
+        require_authenticated()
+
     @expose()
     def _lookup(self, catshortname, *remainder):
         cat = M.TroveCategory.query.get(shortname=catshortname)
         return UserSkillsController(category=cat), remainder
 
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_skills.html')
     def index(self, **kw):
-        require_authenticated()
-
         l = []
         parents = []
         if kw.get('selected_category') is not None:
@@ -324,18 +474,19 @@ class UserSkillsController(BaseController):
             while temp_cat:
                 parents = [temp_cat] + parents
                 temp_cat = temp_cat.parent_category
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
         return dict(
             skills_list = l,
             selected_skill = selected_skill,
             parents = parents,
+            menu = menu,
             add_details_fields=(len(l)==0))
 
     @expose()
     @require_post()
     @validate(F.save_skill_form, error_handler=index)
     def save_skill(self, **kw):
-        require_authenticated()
-        
         trove_id = int(kw.get('selected_skill'))
         category = M.TroveCategory.query.get(trove_cat_id=trove_id)
 
@@ -344,34 +495,37 @@ class UserSkillsController(BaseController):
             level=kw.get('level'),
             comment=kw.get('comment'))
 
-        s = [skill for skill in c.user.skills 
+        s = [skill for skill in c.user.skills
              if str(skill.category_id) != str(new_skill['category_id'])]
         s.append(new_skill)
         c.user.set_pref('skills', s)
         flash('Your skills list was successfully updated!')
-        redirect('/auth/prefs/user_skills')
+        redirect('..')
 
     @expose()
     @require_post()
     @validate(F.remove_skill_form, error_handler=index)
     def remove_skill(self, **kw):
-        require_authenticated()
-
         trove_id = int(kw.get('categoryid'))
         category = M.TroveCategory.query.get(trove_cat_id=trove_id)
 
-        s = [skill for skill in c.user.skills 
+        s = [skill for skill in c.user.skills
              if str(skill.category_id) != str(category._id)]
         c.user.set_pref('skills', s)
         flash('Your skills list was successfully updated!')
-        redirect('/auth/prefs/user_skills')
+        redirect('..')
 
 class UserContactsController(BaseController):
 
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_contacts.html')
     def index(self, **kw):
-        require_authenticated()
-        return dict()
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
 
     @expose()
     @require_post()
@@ -438,10 +592,15 @@ class UserContactsController(BaseController):
 
 class UserAvailabilityController(BaseController):
 
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_availability.html')
     def index(self, **kw):
-        require_authenticated()
-        return dict()
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
 
     @expose()
     @require_post()
@@ -479,16 +638,14 @@ class UserAvailabilityController(BaseController):
         flash('Your availability timeslots were successfully updated!')
         redirect('.')
 
-class PreferencesController(BaseController):
+class SubscriptionsController(BaseController):
 
-    user_skills = UserSkillsController()
-    user_contacts = UserContactsController()
-    user_availability = UserAvailabilityController()
+    def _check_security(self):
+        require_authenticated()
 
     @with_trailing_slash
-    @expose('jinja:allura:templates/user_preferences.html')
+    @expose('jinja:allura:templates/user_subs.html')
     def index(self, **kw):
-        require_authenticated()
         c.form = F.subscription_form
         c.revoke_access = F.oauth_revocation_form
         subscriptions = []
@@ -541,12 +698,10 @@ class PreferencesController(BaseController):
                     frequency=None,
                     artifact=None))
         subscriptions.sort(key=lambda d: (d['project_name'], d['mount_point']))
-        api_token = M.ApiToken.query.get(user_id=c.user._id)
         provider = plugin.AuthenticationProvider.get(request)
         menu = provider.account_navigation()
         return dict(
             subscriptions=subscriptions,
-            api_token=api_token,
             authorized_applications=M.OAuthAccessToken.for_user(c.user),
             menu=menu)
 
@@ -554,43 +709,8 @@ class PreferencesController(BaseController):
     @expose()
     @require_post()
     def update(self,
-               display_name=None,
-               addr=None,
-               new_addr=None,
-               primary_addr=None,
-               oid=None,
-               new_oid=None,
                preferences=None,
                **kw):
-        require_authenticated()
-        if config.get('auth.method', 'local') == 'local':
-            if display_name is None:
-                flash("Display Name cannot be empty.",'error')
-                redirect('.')
-            c.user.set_pref('display_name', display_name)
-            for i, (old_a, data) in enumerate(zip(c.user.email_addresses, addr or [])):
-                obj = c.user.address_object(old_a)
-                if data.get('delete') or not obj:
-                    del c.user.email_addresses[i]
-                    if obj: obj.delete()
-            c.user.set_pref('email_address', primary_addr)
-            if new_addr.get('claim'):
-                if M.EmailAddress.query.get(_id=new_addr['addr'], confirmed=True):
-                    flash('Email address already claimed', 'error')
-                else:
-                    c.user.email_addresses.append(new_addr['addr'])
-                    em = M.EmailAddress.upsert(new_addr['addr'])
-                    em.claimed_by_user_id=c.user._id
-                    em.send_verification_link()
-            for i, (old_oid, data) in enumerate(zip(c.user.open_ids, oid or [])):
-                obj = c.user.openid_object(old_oid)
-                if data.get('delete') or not obj:
-                    del c.user.open_ids[i]
-                    if obj: obj.delete()
-            for k,v in preferences.iteritems():
-                if k == 'results_per_page':
-                    v = int(v)
-                c.user.set_pref(k, v)
         if 'email_format' in preferences:
             c.user.set_pref('email_format', preferences['email_format'])
         redirect('.')
@@ -611,77 +731,6 @@ class PreferencesController(BaseController):
                     s['subscription_id'].delete()
         redirect(request.referer)
 
-    @expose()
-    @require_post()
-    def gen_api_token(self):
-        tok = M.ApiToken.query.get(user_id=c.user._id)
-        if tok is None:
-            tok = M.ApiToken(user_id=c.user._id)
-        else:
-            tok.secret_key = h.cryptographic_nonce()
-        redirect(request.referer)
-
-    @expose()
-    @require_post()
-    def del_api_token(self):
-        tok = M.ApiToken.query.get(user_id=c.user._id)
-        if tok is None: return
-        tok.delete()
-        redirect(request.referer)
-
-    @expose()
-    @require_post()
-    def revoke_oauth(self, _id=None):
-        tok = M.OAuthAccessToken.query.get(_id=bson.ObjectId(_id))
-        if tok is None:
-            flash('Invalid app ID', 'error')
-            redirect('.')
-        if tok.user_id != c.user._id:
-            flash('Invalid app ID', 'error')
-            redirect('.')
-        tok.delete()
-        flash('Application access revoked')
-        redirect('.')
-
-    @expose()
-    @require_post()
-    @validate(V.NullValidator(), error_handler=index)
-    def change_password(self, **kw):
-        kw = g.theme.password_change_form.to_python(kw, None)
-        ap = plugin.AuthenticationProvider.get(request)
-        try:
-            ap.set_password(c.user, kw['oldpw'], kw['pw'])
-        except wexc.HTTPUnauthorized:
-            flash('Incorrect password', 'error')
-            redirect('.')
-        flash('Password changed')
-        redirect('.')
-
-    @expose()
-    @require_post()
-    @validate(F.change_personal_data_form, error_handler=index)
-    def change_personal_data(self, **kw):
-        require_authenticated()
-        c.user.set_pref('sex', kw['sex'])
-        c.user.set_pref('birthdate', kw.get('birthdate'))
-        localization={'country':kw.get('country'), 'city':kw.get('city')}
-        c.user.set_pref('localization', localization)
-        c.user.set_pref('timezone', kw['timezone'])
-
-        flash('Your personal data was successfully updated!')
-        redirect('.')
-
-    @expose()
-    @require_post()
-    def upload_sshkey(self, key=None):
-        ap = plugin.AuthenticationProvider.get(request)
-        try:
-            ap.upload_sshkey(c.user.username, key)
-        except AssertionError, ae:
-            flash('Error uploading key: %s' % ae, 'error')
-        flash('Key uploaded')
-        redirect('.')
-
 class OAuthController(BaseController):
 
     @with_trailing_slash

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 3626167..3e33172 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -137,9 +137,21 @@ class AuthenticationProvider(object):
     def account_navigation(self):
         return [
             {
+                'tabid': 'account_user_prefs',
+                'title': 'Preferences',
+                'target': "/auth/preferences",
+                'alt': 'Manage Personal Preferences',
+            },
+            {
+                'tabid': 'account_user_info',
+                'title': 'Personal Info',
+                'target': "/auth/user_info",
+                'alt': 'Manage Personal Information',
+            },
+            {
                 'tabid': 'account_sfnet_beta_index',
                 'title': 'Subscriptions',
-                'target': "/auth/prefs",
+                'target': "/auth/subscriptions",
                 'alt': 'Manage Subscription Preferences',
             },
         ]
@@ -624,7 +636,7 @@ class ThemeProvider(object):
         :return: None, or an easywidgets Form to render on the user preferences page
         '''
         from allura.lib.widgets.forms import PasswordChangeForm
-        return PasswordChangeForm(action='/auth/prefs/change_password')
+        return PasswordChangeForm(action='/auth/preferences/change_password')
 
     @LazyProperty
     def personal_data_form(self):
@@ -677,7 +689,7 @@ class ThemeProvider(object):
                  allow adding a social network account.
         '''
         from allura.lib.widgets.forms import AddSocialNetworkForm
-        return AddSocialNetworkForm(action='/auth/prefs/add_social_network')
+        return AddSocialNetworkForm(action='/auth/preferences/add_social_network')
 
     @LazyProperty
     def remove_socialnetwork_form(self):
@@ -686,7 +698,7 @@ class ThemeProvider(object):
                  allow removing a social network account.
         '''
         from allura.lib.widgets.forms import RemoveSocialNetworkForm
-        return RemoveSocialNetworkForm(action='/auth/prefs/remove_social_network')
+        return RemoveSocialNetworkForm(action='/auth/preferences/remove_social_network')
 
     @LazyProperty
     def add_timeslot_form(self):
@@ -749,7 +761,7 @@ class ThemeProvider(object):
                  new skill to a user profile
         '''
         from allura.lib.widgets.forms import AddUserSkillForm
-        return AddUserSkillForm(action='/auth/prefs/user_skills/save_skill')
+        return AddUserSkillForm(action='/auth/user_info/skills/save_skill')
 
     @LazyProperty
     def select_subcategory_form(self):
@@ -759,7 +771,7 @@ class ThemeProvider(object):
                  order to see its sub-categories
         '''
         from allura.lib.widgets.forms import SelectSubCategoryForm
-        return SelectSubCategoryForm(action='/auth/prefs/user_skills')
+        return SelectSubCategoryForm(action='/auth/user_info/skills/')
 
     @LazyProperty
     def remove_user_skill(self):
@@ -768,7 +780,7 @@ class ThemeProvider(object):
                  an existing skill from a user profile
         '''
         from allura.lib.widgets.forms import RemoveSkillForm
-        return RemoveSkillForm(action='/auth/prefs/user_skills/remove_skill')
+        return RemoveSkillForm(action='/auth/user_info/skills/remove_skill')
 
     @LazyProperty
     def upload_key_form(self):
@@ -776,7 +788,7 @@ class ThemeProvider(object):
         :return: None, or an easywidgets Form to render on the user preferences page
         '''
         from allura.lib.widgets.forms import UploadKeyForm
-        return UploadKeyForm(action='/auth/prefs/upload_sshkey')
+        return UploadKeyForm(action='/auth/preferences/upload_sshkey')
 
     @property
     def master(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/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 1b28457..cc503f4 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -2801,3 +2801,80 @@ table thead tr th.narrow, table tr td.narrow {
 #selected-projects {
   padding: 10px 10px;
 }
+#account-nav-menu {
+  background-color: #e5e5e5;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  overflow: hidden;
+  *zoom: 1;
+  background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #cccccc));
+  background: -webkit-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -moz-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -o-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -ms-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  -moz-border-radius: 4px;
+  -webkit-border-radius: 4px;
+  -o-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -khtml-border-radius: 4px;
+  border-radius: 4px;
+  -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  -o-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  margin: 0 10px 1em;
+}
+#account-nav-menu li {
+  list-style-image: none;
+  list-style-type: none;
+  margin-left: 0;
+  display: -moz-inline-box;
+  -moz-box-orient: vertical;
+  display: inline-block;
+  vertical-align: middle;
+  *vertical-align: auto;
+  white-space: nowrap;
+}
+#account-nav-menu li {
+  *display: inline;
+}
+#account-nav-menu a {
+  display: -moz-inline-box;
+  -moz-box-orient: vertical;
+  display: inline-block;
+  vertical-align: middle;
+  *vertical-align: auto;
+  color: inherit;
+  text-decoration: inherit;
+  cursor: inherit;
+  cursor: pointer;
+  padding: 10px;
+  padding-bottom: 0;
+  color: #555;
+  font-family: Ubuntu, sans-serif;
+}
+#account-nav-menu a {
+  *display: inline;
+}
+#account-nav-menu a:active, #account-nav-menu a:focus {
+  outline: none;
+}
+#account-nav-menu .marker {
+  width: 10px;
+  height: 10px;
+}
+#account-nav-menu .marker.current {
+  -moz-transform: rotate(45deg);
+  -webkit-transform: rotate(45deg);
+  -o-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  background: white;
+  border-left: 1px solid #AAA;
+  border-top: 1px solid #AAA;
+  position: relative;
+  bottom: -5px;
+  left: 45%;
+}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/jinja_master/theme_macros.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/jinja_master/theme_macros.html b/Allura/allura/templates/jinja_master/theme_macros.html
index 655b67b..d0d28ed 100644
--- a/Allura/allura/templates/jinja_master/theme_macros.html
+++ b/Allura/allura/templates/jinja_master/theme_macros.html
@@ -3,7 +3,7 @@
     <div class="wrapper">
         <nav>
           {% if c.user._id %}
-            <a href="/auth/prefs/">Account</a>
+            <a href="/auth/preferences/">Account</a>
             <a href="{{c.user.url()}}">{{name}}</a>
             <a href="{{logout_url}}">Log Out</a>
           {% else %}
@@ -34,7 +34,7 @@
     function _add_tracking(prefix, tracking_id) {
         _gaq.push(
             [prefix+'._setAccount', tracking_id],
-            [prefix+'._trackPageview'],
+            [prefix+'._trackPageview']
         );
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/oauth_authorize_ok.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/oauth_authorize_ok.html b/Allura/allura/templates/oauth_authorize_ok.html
index 33af743..5f02f54 100644
--- a/Allura/allura/templates/oauth_authorize_ok.html
+++ b/Allura/allura/templates/oauth_authorize_ok.html
@@ -8,8 +8,8 @@
 {% block content %}
 <p>You have authorized {{ rtok.consumer_token.name }} access to your account.  If you wish
   to revoke this access at any time, please visit 
-  <a href="{{g.url('/auth/prefs/') }}">user preferences</a>
+  <a href="{{g.url('/auth/preferences/') }}">user preferences</a>
   and click 'revoke access'.</p>
 <h2>PIN: {{ rtok.validation_pin }}</h2>
-<a href="{{g.url('/auth/prefs/')}}">Return to preferences</a>
+<a href="{{g.url('/auth/preferences/')}}">Return to preferences</a>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/trovecategories.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/trovecategories.html b/Allura/allura/templates/trovecategories.html
index b97f20b..ae1b1e5 100644
--- a/Allura/allura/templates/trovecategories.html
+++ b/Allura/allura/templates/trovecategories.html
@@ -53,7 +53,7 @@
       {{g.theme.add_trove_category.display(uppercategory_id=0)}}
     {% endif %}
     <div class="grid-20" style="margin-bottom:10px;">
-      Are you done creating new categories? <a href="/auth/prefs/user_skills/{{selected_cat.shortname}}">Click here</a> to configure your skills!
+      Are you done creating new categories? <a href="/auth/user_info/skills/{{selected_cat.shortname}}">Click here</a> to configure your skills!
     </div>
 
   </div>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_availability.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_availability.html b/Allura/allura/templates/user_availability.html
index d7f46b7..54778b7 100644
--- a/Allura/allura/templates/user_availability.html
+++ b/Allura/allura/templates/user_availability.html
@@ -6,15 +6,32 @@
 {% block header %}Availability timeslots of {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+  </ul>
+
   <div class="grid-20">
     <h2>Availability</h2>
     <div class="grid-18">
-      Please, set your time intervals choosing a weekday and entering the time interval according to the timezone specified in your personal data, using the format HH:MM. If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored.
+        Please, set your time intervals choosing a weekday and entering the
+        time interval according to the timezone specified in your personal
+        data, using the format HH:MM. If you didn't set any timezone, your
+        timeslots could be meaningless to other users, therefore they will
+        be ignored.
     </div>
     <div class="grid-18">
-      You can also specify periods of time during which you won't be able to work on the forge, in orther to communicate other users
-      that they can't contact you during those days. Please, do it specifying date intervals in format DD/MM/YYYY.
-    </div> 
+        You can also specify periods of time during which you won't be able
+        to work on the forge, in orther to communicate other users that they
+        can't contact you during those days. Please, do it specifying date
+        intervals in format DD/MM/YYYY.
+    </div>
   </div>
   <div class="grid-20">
     {%if c.user.get_availability_timeslots() %}
@@ -30,15 +47,15 @@
         </tr>
         {% for ts in c.user.get_availability_timeslots() %}
           {{g.theme.remove_timeslot_form.display(
-                action="/auth/prefs/user_availability/remove_timeslot",
+                action="remove_timeslot",
                 weekday=ts.week_day,
                 starttime=ts.start_time,
-                endtime=ts.end_time)}} 
+                endtime=ts.end_time)}}
         {%endfor%}
       </table>
     {% endif %}
     <h3>Add a new availability timeslot</h3>
-    {{g.theme.add_timeslot_form.display(action="/auth/prefs/user_availability/add_timeslot")}}
+    {{g.theme.add_timeslot_form.display(action="add_timeslot")}}
   </div>
 
   <div class="grid-20">
@@ -54,21 +71,13 @@
         </tr>
         {% for ip in c.user.get_inactive_periods() %}
           {{g.theme.remove_inactive_period_form.display(
-                action="/auth/prefs/user_availability/remove_inactive_period",
+                action="remove_inactive_period",
                 startdate=ip.start_date,
-                enddate=ip.end_date)}} 
+                enddate=ip.end_date)}}
         {%endfor%}
       </table>
     {% endif %}
     <h3>Add a new period of inactivity on the forge</h3>
-    {{g.theme.add_inactive_period_form.display(action="/auth/prefs/user_availability/add_inactive_period")}}
-    <h3>Other possible actions</h3>
-    <div class="grid-20" style="margin-bottom:10px;"/>
-      <ul>
-        <li>
-          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
-        </li>
-      </ul>
-    </div>
+    {{g.theme.add_inactive_period_form.display(action="add_inactive_period")}}
   </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_contacts.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_contacts.html b/Allura/allura/templates/user_contacts.html
index d6d1811..9de0f24 100644
--- a/Allura/allura/templates/user_contacts.html
+++ b/Allura/allura/templates/user_contacts.html
@@ -6,13 +6,24 @@
 {% block header %}Contacts of {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
   <div class="grid-20">
     <h2>Personal Contacts</h2>
     <h3>Skype account</h3>
 
-    {{g.theme.skype_account_form.display(action="/auth/prefs/user_contacts/skype_account",
+    {{g.theme.skype_account_form.display(action="skype_account",
           initial_value=c.user.get_pref('skypeaccount'))}}
-     
+
     {%if c.user.get_pref('socialnetworks') or c.user.get_pref('telnumbers') or c.user.get_pref('webpages') %}
       <h3>Other existing contacts</h3>
         <table>
@@ -24,32 +35,24 @@
             </thead>
           </tr>
           {% for sn in c.user.get_pref('socialnetworks') %}
-             {{g.theme.remove_socialnetwork_form.display(action="/auth/prefs/user_contacts/remove_social_network", account=sn.accounturl, socialnetwork=sn.socialnetwork)}} 
+             {{g.theme.remove_socialnetwork_form.display(action="remove_social_network", account=sn.accounturl, socialnetwork=sn.socialnetwork)}}
           {% endfor %}
 
           {% for tn in c.user.get_pref('telnumbers') %}
-              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/user_contacts/remove_telnumber", value=tn, label="Telephone number")}} 
+              {{g.theme.remove_textvalue_form.display(action="remove_telnumber", value=tn, label="Telephone number")}}
           {%endfor%}
 
           {% for ws in c.user.get_pref('webpages') %}
-              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/user_contacts/remove_webpage", value=ws, label="Website url")}} 
+              {{g.theme.remove_textvalue_form.display(action="remove_webpage", value=ws, label="Website url")}}
           {%endfor%}
         </table>
     {% endif %}
 
     <h3>Add a social network account</h3>
-    {{g.theme.add_socialnetwork_form.display(action="/auth/prefs/user_contacts/add_social_network")}}
+    {{g.theme.add_socialnetwork_form.display(action="add_social_network")}}
     <h3>Add a telephone number</h3>
-    {{g.theme.add_telnumber_form.display(action="/auth/prefs/user_contacts/add_telnumber")}}
+    {{g.theme.add_telnumber_form.display(action="add_telnumber")}}
     <h3>Add a personal website</h3>
-    {{g.theme.add_website_form.display(action="/auth/prefs/user_contacts/add_webpage")}}
-  <h3>Other possible actions</h3>
-    <div class="grid-20" style="margin-bottom:10px;"/>
-      <ul>
-        <li>
-          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
-        </li>
-      </ul>
-    </div>
+    {{g.theme.add_website_form.display(action="add_webpage")}}
   </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_info.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_info.html b/Allura/allura/templates/user_info.html
new file mode 100644
index 0000000..6fe112e
--- /dev/null
+++ b/Allura/allura/templates/user_info.html
@@ -0,0 +1,47 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Information</h2>
+    {{g.theme.personal_data_form.display(action="/auth/user_info/change_personal_data", user=c.user)}} 
+  </div>
+
+  <a name="Contacts"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Contacts</h2>
+    <p>
+      If you want, you can set your contacts allowing other members to contact you or you can set your personal website allowing other members to introduce your self.
+    </p>
+    <ul><li><a href="contacts">Click here to check and change your contacts</a></li></ul>
+  </div>
+
+  <a name="Availability"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Availability</h2>
+    <p>
+      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge. (If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored!)
+    </p>
+    <ul><li><a href="availability">Click here to check and change your availability slots</a></li></ul>
+  </div>
+
+  <div class="grid-20">
+    <h2>Skills list</h2>
+    <ul><li><a href="skills">Click here to check and change your skills list</a></li></ul>
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
deleted file mode 100644
index 4e1391e..0000000
--- a/Allura/allura/templates/user_preferences.html
+++ /dev/null
@@ -1,195 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}{{c.user.username}} / Preferences{% endblock %}
-
-{% block header %}User Preferences for {{c.user.username}}{% endblock %}
-
-{% block content %}
-  <ul id="account-nav-menu" class="b-hornav droppy">
-      {% for item in menu -%}
-      <li id="{{ item.tabid }}">
-      <a href="{{ item.target }}">
-          {{ item.title }}
-          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
-      </a>
-      </li>
-      {%- endfor %}
-   </ul>
-
-  <div style="clear:both" class="grid-20">
-    <h2>Personal Settings</h2>
-    {{g.theme.personal_data_form.display(action="/auth/prefs/change_personal_data", user=c.user)}} 
-  </div>
-
-  <div style="clear:both" class="grid-20">
-    <a name="Contacts"></a>
-    <h2>Personal Contacts</h2>
-    <p>
-      If you want, you can set your contacts allowing other members to contact you or you can set your personal website allowing other members to introduce your self.
-    </p>
-    <ul><li><a href="/auth/prefs/user_contacts">Click here to check and change your contacts</a></li></ul>
-  </div>
-
-  <a name="Availability"></a>
-  <div style="clear:both" class="grid-20">
-    <h2>Availability</h2>
-    <p>
-      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge. (If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored!)
-    </p>
-    <ul><li><a href="/auth/prefs/user_availability">Click here to check and change your availability slots</a></li></ul>
-  </div>
-
-  <div class="grid-20">
-    <h2>Skills list</h2>
-    <ul><li><a href="/auth/prefs/user_skills">Click here to check and change your skills list</a></li></ul>
-  </div>
-
-  {% if g.theme.password_change_form %}
-  <div class="grid-20">
-    <h2>Change Password</h2>
-    {{ g.theme.password_change_form.display() }}
-  </div>
-  {% endif %}
-  {% if g.theme.upload_key_form %}
-  <div class="grid-20">
-    <h2>Upload ssh public key</h2>
-    {{ g.theme.upload_key_form.display() }}
-  </div>
-  {% endif %}
-
-  {% if tg.config.get('auth.method', 'local') == 'local' %}
-      <br style="clear:both"/>
-      <h2>API Token</h2>
-      {% if api_token %}
-        <p>
-          <b>API Key:</b><br/>
-          {{api_token.api_key}}<br/>
-          <b>Secret Key:</b><br/>
-          {{api_token.secret_key}}<br/>
-        </p>
-        <form method="POST" action="del_api_token" class="grid-18">
-          <input type="submit" value="Delete API Token">
-        </form>
-      {% else %}
-        <p>No API token generated</p>
-      {% endif %}
-      <form method="POST" action="gen_api_token" class="grid-18">
-        <input type="submit" value="(Re)generate API Token">
-      </form>
-  {% endif %}
-
-  <div style="clear:both"></div>
-
-  <h2>Authorized Third-party Applications</h2>
-  {% for access_tok in authorized_applications %}
-    <div>
-      <h3>{{access_tok.consumer_token.name}}</h3>
-      {{access_tok.consumer_token.description_html}}
-      {{ c.revoke_access.display(value=access_tok) }}
-      <br style="clear:both"/>
-  </div>
- {% endfor %}
-    {% if not authorized_applications %}<p>No authorized third-party applications</p>{% endif %}
-
-
-  <h2>Subscriptions</h2>
-  {% if subscriptions %}
-    <p><em>Mark tools that you want to subscribe to. Unmark tools that you want to unsubscribe from. Press 'Save' button.</em></p>
-    {{c.form.display(action='update_subscriptions', value=dict(subscriptions=subscriptions))}}
-  {% else%}
-    <p>No subscriptions.</p>
-  {% endif %}
-  <hr/>
-  <div style="clear:both">&nbsp;</div>
-  <form action="update" method="post">
-        {% if tg.config.get('auth.method', 'local') == 'local' %}
-        <label class="grid-4">Display Name</label>
-        <div class="grid-18">
-          <input name="preferences.display_name" value="{{c.user.display_name}}" type="text">
-        </div>
-        {% endif %}
-        <label class="grid-4">Email Format</label>
-        <div class="grid-18">
-          <select name="preferences.email_format">
-            <option value="plain" {{'selected' if c.user.preferences.email_format == 'plain' else ''}}>Plain Text</option>
-            <option value="html" {{'selected' if c.user.preferences.email_format == 'html' else ''}}>HTML</option>
-            <option value="both" {{'selected' if c.user.preferences.email_format == 'both' else ''}}>Combined</option>
-          </select>
-        </div>
-        {% if tg.config.get('auth.method', 'local') == 'local' %}
-        <label class="grid-4">Page Size</label>
-        <div class="grid-18">
-          <select name="preferences.results_per_page">
-            {% for per_page in [25, 50, 100, 250] %}
-                <option {% if per_page == c.user.preferences.results_per_page %}selected="selected"{% endif %}
-                   value="{{per_page}}">{{per_page}}</option>
-            {% endfor %}
-          </select>
-        </div>
-        {% endif %}
-
-    {% if tg.config.get('auth.method', 'local') == 'local' %}
-      {% for a in c.user.email_addresses %}
-        <input name="addr-{{loop.index0}}.ord" value="{{loop.index0}}" type="hidden"/>
-      {% endfor %}
-      {% if c.user.email_addresses %}
-        <h3 class="grid-18">Email Addresses</h3>
-        <table class="grid-18">
-          <tr>
-            <th>Primary?</th>
-            <th>Address</th>
-            <th>Confirmed</th>
-            <th></th>
-          </tr>
-          {% for a in c.user.email_addresses %}
-          <tr>
-            {% set obj = c.user.address_object(a) %}
-            <td>{{lib.radio_button('primary_addr', None, a, c.user.preferences.email_address)}}</td>
-            <td>{{a}}</td>
-            {% if obj %}
-            <td>
-              {% if obj.confirmed %}
-                yes
-              {% else %}
-                no (<a href="{{g.url('/auth/send_verification_link', a=a)}}">verify</a>)
-              {% endif %}
-            </td>
-            {% else %}
-              <td>Unknown addr obj {{a}}</td>
-            {% endif %}
-            <td>{{lib.submit_button('Delete', 'addr-%s.delete' % i)}}</td>
-          </tr>
-          {% endfor %}
-        </table>
-        {% endif %}
-        <div class="grid-18">
-        {{lib.text_field('new_addr.addr', 'New Email Address')}}
-        {{lib.submit_button('Claim Address', name='new_addr.claim')}}
-        </div>
-
-        {% if c.user.open_ids %}
-        <h3 class="grid-18">OpenIDs Claimed</h3>
-        <table class="grid-18">
-          <tr>
-            <th>OpenID</th>
-            <th></th>
-          </tr>
-          {% for oid in c.user.open_ids %}
-            {% set obj = c.user.openid_object(oid) %}
-          <tr>
-            <td>{{oid}}</td>
-            <td>{{lib.submit_button('Delete', 'oid-%s.delete' % loop.index0)}}</td>
-          </tr>
-          {% endfor %}
-        </table>
-        {% endif %}
-        <div class="grid-18">
-        <a href="/auth/claim_oid">Claim New OpenID</a>
-        </div>
-    {% endif %}
-    <div class="grid-18">
-    {{lib.submit_button('Save Changes')}}
-    </div>
-  </form>
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_prefs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_prefs.html b/Allura/allura/templates/user_prefs.html
new file mode 100644
index 0000000..0da9c9f
--- /dev/null
+++ b/Allura/allura/templates/user_prefs.html
@@ -0,0 +1,140 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+  <div class="grid-23">
+      <h2>Preferences</h2>
+      <form action="update" method="post">
+            {% if tg.config.get('auth.method', 'local') == 'local' %}
+            <label class="grid-4">Display Name</label>
+            <div class="grid-18">
+              <input name="preferences.display_name" value="{{c.user.display_name}}" type="text">
+            </div>
+            <label class="grid-4">Page Size</label>
+            <div class="grid-18">
+              <select name="preferences.results_per_page">
+                {% for per_page in [25, 50, 100, 250] %}
+                    <option {% if per_page == c.user.preferences.results_per_page %}selected="selected"{% endif %}
+                       value="{{per_page}}">{{per_page}}</option>
+                {% endfor %}
+              </select>
+            </div>
+            {% endif %}
+
+        {% if tg.config.get('auth.method', 'local') == 'local' %}
+          {% for a in c.user.email_addresses %}
+            <input name="addr-{{loop.index0}}.ord" value="{{loop.index0}}" type="hidden"/>
+          {% endfor %}
+          {% if c.user.email_addresses %}
+            <h3 class="grid-18">Email Addresses</h3>
+            <table class="grid-18">
+              <tr>
+                <th>Primary?</th>
+                <th>Address</th>
+                <th>Confirmed</th>
+                <th></th>
+              </tr>
+              {% for a in c.user.email_addresses %}
+              <tr>
+                {% set obj = c.user.address_object(a) %}
+                <td>{{lib.radio_button('primary_addr', None, a, c.user.preferences.email_address)}}</td>
+                <td>{{a}}</td>
+                {% if obj %}
+                <td>
+                  {% if obj.confirmed %}
+                    yes
+                  {% else %}
+                    no (<a href="{{g.url('/auth/send_verification_link', a=a)}}">verify</a>)
+                  {% endif %}
+                </td>
+                {% else %}
+                  <td>Unknown addr obj {{a}}</td>
+                {% endif %}
+                <td>{{lib.submit_button('Delete', 'addr-%s.delete' % loop.index0)}}</td>
+              </tr>
+              {% endfor %}
+            </table>
+            {% endif %}
+            <div class="grid-18">
+            {{lib.text_field('new_addr.addr', 'New Email Address')}}
+            {{lib.submit_button('Claim Address', name='new_addr.claim')}}
+            </div>
+
+            {% if c.user.open_ids %}
+            <h3 class="grid-18">OpenIDs Claimed</h3>
+            <table class="grid-18">
+              <tr>
+                <th>OpenID</th>
+                <th></th>
+              </tr>
+              {% for oid in c.user.open_ids %}
+                {% set obj = c.user.openid_object(oid) %}
+              <tr>
+                <td>{{oid}}</td>
+                <td>{{lib.submit_button('Delete', 'oid-%s.delete' % loop.index0)}}</td>
+              </tr>
+              {% endfor %}
+            </table>
+            {% endif %}
+            <div class="grid-18">
+            <a href="/auth/claim_oid">Claim New OpenID</a>
+            </div>
+        {% endif %}
+        <div class="grid-18">
+        {{lib.submit_button('Save Changes')}}
+        </div>
+      </form>
+  </div>
+
+   <div style="clear:both"></div>
+  {% if g.theme.password_change_form %}
+  <div class="grid-20">
+    <h2>Change Password</h2>
+    {{ g.theme.password_change_form.display() }}
+  </div>
+  {% endif %}
+
+  {% if g.theme.upload_key_form %}
+  <div class="grid-20">
+    <h2>Upload ssh public key</h2>
+    {{ g.theme.upload_key_form.display() }}
+  </div>
+  {% endif %}
+
+  {% if tg.config.get('auth.method', 'local') == 'local' %}
+  <div class="grid-20">
+      <h2>API Token</h2>
+      {% if api_token %}
+        <p>
+          <b>API Key:</b><br/>
+          {{api_token.api_key}}<br/>
+          <b>Secret Key:</b><br/>
+          {{api_token.secret_key}}<br/>
+        </p>
+        <form method="POST" action="del_api_token" class="grid-18">
+          <input type="submit" value="Delete API Token">
+        </form>
+      {% else %}
+        <p>No API token generated</p>
+      {% endif %}
+      <form method="POST" action="gen_api_token" class="grid-18">
+        <input type="submit" value="(Re)generate API Token">
+      </form>
+  </div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_skills.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_skills.html b/Allura/allura/templates/user_skills.html
index 667ff70..bf58d19 100644
--- a/Allura/allura/templates/user_skills.html
+++ b/Allura/allura/templates/user_skills.html
@@ -6,6 +6,17 @@
 {% block header %}Skills manager for {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+  </ul>
+
   <div class="grid-20">
     {% if c.user.get_skills()|length > 0 %}
       <h2>Your current skills list:</h2>
@@ -43,9 +54,9 @@
           You selected:
         </div>
         <div class="grid-12" style="margin-bottom:20px">      
-           <a href="user_skills">List of all skills</a>
+           <a href=".">List of all skills</a>
            {% for cat in parents %}
-             &gt; <a href="/auth/prefs/user_skills/{{cat.shortname}}">{{cat.fullname}}</a>
+             &gt; <a href="{{cat.shortname}}">{{cat.fullname}}</a>
            {% endfor %}
            &gt; <b>{{selected_skill.fullname}}</b>
            <input type="hidden" name="upper_category" value="{{selected_skill.trove_parent_id}}"/>
@@ -64,23 +75,7 @@
     {% if selected_skill %}
       <h3>Add "{{selected_skill.fullname}}" to you set of skills</h3>
       {{g.theme.add_user_skill.display(selected_skill=selected_skill.trove_cat_id, 
-           action="/auth/prefs/user_skills/" + selected_skill.shortname + "/save_skill")}}
+           action=selected_skill.shortname + "/save_skill")}}
     {% endif %}
-    <h3>Other possible actions</h3>
-    <div class="grid-20" style="margin-bottom:10px;"/>
-      <ul>
-        {%if tg.config.get('trovecategories.enableediting', 'false')=='true'%}
-          <li>
-            <a href="/categories/{{selected_skill.shortname}}">
-              Create a new category in this list
-            </a>
-            if you want to add a more specific kind of skill which is not included here.
-          </li>
-        {%endif%}
-        <li>
-          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
-        </li>
-      </ul>
-    </div>
   </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_subs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_subs.html b/Allura/allura/templates/user_subs.html
new file mode 100644
index 0000000..457e7a7
--- /dev/null
+++ b/Allura/allura/templates/user_subs.html
@@ -0,0 +1,54 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+   <h2>Authorized Third-party Applications</h2>
+   {% for access_tok in authorized_applications %}
+     <div>
+       <h3>{{access_tok.consumer_token.name}}</h3>
+       {{access_tok.consumer_token.description_html}}
+       {{ c.revoke_access.display(value=access_tok) }}
+       <br style="clear:both"/>
+   </div>
+  {% endfor %}
+     {% if not authorized_applications %}<p>No authorized third-party applications</p>{% endif %}
+
+  <h2>Subscriptions</h2>
+  {% if subscriptions %}
+    <p><em>Mark tools that you want to subscribe to. Unmark tools that you want to unsubscribe from. Press 'Save' button.</em></p>
+    {{c.form.display(action='update_subscriptions', value=dict(subscriptions=subscriptions))}}
+  {% else%}
+    <p>No subscriptions.</p>
+  {% endif %}
+  <hr/>
+  <div style="clear:both">&nbsp;</div>
+  <form action="update" method="post">
+        <label class="grid-4">Email Format</label>
+        <div class="grid-18">
+          <select name="preferences.email_format">
+            <option value="plain" {{'selected' if c.user.preferences.email_format == 'plain' else ''}}>Plain Text</option>
+            <option value="html" {{'selected' if c.user.preferences.email_format == 'html' else ''}}>HTML</option>
+            <option value="both" {{'selected' if c.user.preferences.email_format == 'both' else ''}}>Combined</option>
+          </select>
+        </div>
+
+    <div class="grid-18">
+    {{lib.submit_button('Save Changes')}}
+    </div>
+  </form>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/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 0b4bb1c..666c3ec 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -41,19 +41,19 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs(self):
-        r = self.app.get('/auth/prefs/', extra_environ=dict(username='test-admin'))
+        r = self.app.get('/auth/preferences/', extra_environ=dict(username='test-admin'))
         assert 'test@example.com' not in r
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'new_addr.addr':'test@example.com',
                  'new_addr.claim':'Claim Address',
                  'primary_addr':'test-admin@users.localhost',
                  'preferences.email_format':'plain'},
                 extra_environ=dict(username='test-admin'))
-        r = self.app.get('/auth/prefs/')
+        r = self.app.get('/auth/preferences/')
         assert 'test@example.com' in r
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'addr-1.ord':'1',
                  'addr-2.ord':'1',
                  'addr-2.delete':'on',
@@ -61,13 +61,13 @@ class TestAuth(TestController):
                  'primary_addr':'test-admin@users.localhost',
                  'preferences.email_format':'plain'},
                 extra_environ=dict(username='test-admin'))
-        r = self.app.get('/auth/prefs/')
+        r = self.app.get('/auth/preferences/')
         assert 'test@example.com' not in r
         ea = M.EmailAddress.query.get(_id='test-admin@users.localhost')
         ea.confirmed = True
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'new_addr.addr':'test-admin@users.localhost',
                  'new_addr.claim':'Claim Address',
                  'primary_addr':'test-admin@users.localhost',
@@ -76,7 +76,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions(self):
-        r = self.app.get('/auth/prefs/',
+        r = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         subscriptions = M.Mailbox.query.find(dict(
             user_id=c.user._id, is_flash=False)).all()
@@ -118,7 +118,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_subscribe(self):
-        resp = self.app.get('/auth/prefs/',
+        resp = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         # find not subscribed tool, subscribe and verify
@@ -134,7 +134,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_unsubscribe(self):
-        resp = self.app.get('/auth/prefs/',
+        resp = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         field_name = self._find_subscriptions_field(form, subscribed=True)
@@ -147,15 +147,15 @@ class TestAuth(TestController):
         assert not s, "User still has subscription with Mailbox._id %s" % s_id
 
     def test_api_key(self):
-         r = self.app.get('/auth/prefs/')
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' in r
-         r = self.app.post('/auth/prefs/gen_api_token', status=302)
-         r = self.app.get('/auth/prefs/')
+         r = self.app.post('/auth/preferences/gen_api_token', status=302)
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' not in r
          assert 'API Key:' in r
          assert 'Secret Key:' in r
-         r = self.app.post('/auth/prefs/del_api_token', status=302)
-         r = self.app.get('/auth/prefs/')
+         r = self.app.post('/auth/preferences/del_api_token', status=302)
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' in r
 
     def test_oauth(self):
@@ -382,10 +382,10 @@ class TestPreferences(TestController):
         from pytz import country_names
         setsex, setbirthdate, setcountry, setcity, settimezone = \
             ('Male', '19/08/1988', 'IT', 'Milan', 'Europe/Rome')
-        result = self.app.get('/auth/prefs')
+        result = self.app.get('/auth/user_info')
 
         #Check if personal data is properly set
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(
                  sex=setsex,
                  birthdate=setbirthdate,
@@ -405,7 +405,7 @@ class TestPreferences(TestController):
         assert timezone == settimezone
 
         #Check if setting a wrong date everything works correctly
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(birthdate='30/02/1998'))
         assert 'Please enter a valid date' in str(r)
         user = M.User.query.get(username='test-admin')
@@ -421,7 +421,7 @@ class TestPreferences(TestController):
         assert timezone == settimezone
 
         #Check deleting birthdate
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(
                  sex=setsex,
                  birthdate='',
@@ -435,8 +435,8 @@ class TestPreferences(TestController):
     def test_contacts(self):
         #Add skype account
         testvalue = 'testaccount'
-        result = self.app.get('/auth/prefs/user_contacts')
-        r = self.app.post('/auth/prefs/user_contacts/skype_account',
+        result = self.app.get('/auth/user_info/contacts')
+        r = self.app.post('/auth/user_info/contacts/skype_account',
              params=dict(skypeaccount=testvalue))
         user = M.User.query.get(username='test-admin')
         assert user.skypeaccount == testvalue
@@ -444,7 +444,7 @@ class TestPreferences(TestController):
         #Add social network account
         socialnetwork = 'Facebook'
         accounturl = 'http://www.facebook.com/test'
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork,
                          accounturl = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -455,7 +455,7 @@ class TestPreferences(TestController):
         #Add second social network account
         socialnetwork2 = 'Twitter'
         accounturl2 = 'http://twitter.com/test'
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork2,
                          accounturl = '@test'))
         user = M.User.query.get(username='test-admin')
@@ -464,7 +464,7 @@ class TestPreferences(TestController):
                 {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks)
 
         #Remove first social network account
-        r = self.app.post('/auth/prefs/user_contacts/remove_social_network',
+        r = self.app.post('/auth/preferences/contacts/remove_social_network',
              params=dict(socialnetwork=socialnetwork,
                          account = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -472,7 +472,7 @@ class TestPreferences(TestController):
                {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks
 
         #Add invalid social network account
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(accounturl = accounturl, socialnetwork=''))
         user = M.User.query.get(username='test-admin')
         assert len(user.socialnetworks) == 1 and \
@@ -480,40 +480,40 @@ class TestPreferences(TestController):
 
         #Add telephone number
         telnumber = '+3902123456'
-        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
+        r = self.app.post('/auth/preferences/contacts/add_telnumber',
              params=dict(newnumber=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and (user.telnumbers[0] == telnumber))
 
         #Add second telephone number
         telnumber2 = '+3902654321'
-        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
+        r = self.app.post('/auth/preferences/contacts/add_telnumber',
              params=dict(newnumber=telnumber2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 2 and telnumber in user.telnumbers and telnumber2 in user.telnumbers)
 
         #Remove first telephone number
-        r = self.app.post('/auth/prefs/user_contacts/remove_telnumber',
+        r = self.app.post('/auth/preferences/contacts/remove_telnumber',
              params=dict(oldvalue=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and telnumber2 in user.telnumbers)
 
         #Add website
         website = 'http://www.testurl.com'
-        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
+        r = self.app.post('/auth/preferences/contacts/add_webpage',
              params=dict(newwebsite=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and (website in user.webpages))
 
         #Add second website
         website2 = 'http://www.testurl2.com'
-        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
+        r = self.app.post('/auth/preferences/contacts/add_webpage',
              params=dict(newwebsite=website2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 2 and website in user.webpages and website2 in user.webpages)
 
         #Remove first website
-        r = self.app.post('/auth/prefs/user_contacts/remove_webpage',
+        r = self.app.post('/auth/preferences/contacts/remove_webpage',
              params=dict(oldvalue=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and website2 in user.webpages)
@@ -527,8 +527,8 @@ class TestPreferences(TestController):
         starttime = time(9,0,0)
         endtime = time(12, 0, 0)
 
-        result = self.app.get('/auth/prefs/user_availability')
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        result = self.app.get('/auth/preferences/availability')
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -542,7 +542,7 @@ class TestPreferences(TestController):
         endtime2 = time(16, 0, 0)
 
         #Add second availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=starttime2.strftime('%H:%M'),
@@ -553,7 +553,7 @@ class TestPreferences(TestController):
                and timeslot2dict in user.get_availability_timeslots()
 
         #Remove availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/remove_timeslot',
+        r = self.app.post('/auth/preferences/availability/remove_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -562,7 +562,7 @@ class TestPreferences(TestController):
         assert len(user.availability) == 1 and timeslot2dict in user.get_availability_timeslots()
 
         #Add invalid availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=endtime2.strftime('%H:%M'),
@@ -581,8 +581,8 @@ class TestPreferences(TestController):
         now = datetime(now.year, now.month, now.day)
         startdate = now + timedelta(days=1)
         enddate = now + timedelta(days=7)
-        result = self.app.get('/auth/prefs/user_availability')
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        result = self.app.get('/auth/preferences/availability')
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -593,7 +593,7 @@ class TestPreferences(TestController):
         #Add second inactivity period
         startdate2 =  now + timedelta(days=24)
         enddate2 = now + timedelta(days=28)
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate=startdate2.strftime('%d/%m/%Y'),
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -603,7 +603,7 @@ class TestPreferences(TestController):
                and period2dict in user.get_inactive_periods()
 
         #Remove first inactivity period
-        r = self.app.post('/auth/prefs/user_availability/remove_inactive_period',
+        r = self.app.post('/auth/preferences/availability/remove_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -611,7 +611,7 @@ class TestPreferences(TestController):
         assert len(user.inactiveperiod) == 1 and period2dict in user.get_inactive_periods()
 
         #Add invalid inactivity period
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate='NOT/A/DATE',
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -627,8 +627,8 @@ class TestPreferences(TestController):
         skill_cat = M.TroveCategory.query.get(show_as_skill=True)
         level = 'low'
         comment = 'test comment'
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -640,8 +640,8 @@ class TestPreferences(TestController):
         #Add again the same skill
         level = 'medium'
         comment = 'test comment 2'
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -653,7 +653,7 @@ class TestPreferences(TestController):
         #Add an invalid skill
         level2 = 'not a level'
         comment2 = 'test comment 2'
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level2,
                  comment=comment2,
@@ -663,8 +663,8 @@ class TestPreferences(TestController):
         assert len(user.skills) == 1 and skilldict in user.skills
 
         #Remove a skill
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/remove_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/remove_skill',
              params=dict(
                  categoryid=str(skill_cat.trove_cat_id)))
         user = M.User.query.get(username='test-admin')


[19/48] git commit: [5453] adding support for user stats

Posted by tv...@apache.org.
[5453] adding support for user stats


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

Branch: refs/heads/si/5453
Commit: 25f7657c7ba8ff0287a790a6c6a562a0a56625b6
Parents: b849642
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Dec 12 22:06:15 2012 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/discuss.py               |    1 +
 Allura/allura/controllers/root.py                  |    3 +
 Allura/allura/eventslistener.py                    |   27 +
 .../ext/user_profile/templates/user_index.html     |    9 +
 Allura/allura/ext/user_profile/user_main.py        |    9 +-
 Allura/allura/lib/app_globals.py                   |   13 +
 Allura/allura/lib/graphics/graphic_methods.py      |   69 ++
 Allura/allura/model/artifact.py                    |   11 +-
 Allura/allura/model/auth.py                        |   18 +
 Allura/allura/model/discuss.py                     |    2 +-
 Allura/allura/model/repo_refresh.py                |    9 +
 Allura/allura/public/nf/images/down.png            |  Bin 0 -> 2993 bytes
 Allura/allura/public/nf/images/equal.png           |  Bin 0 -> 343 bytes
 Allura/allura/public/nf/images/up.png              |  Bin 0 -> 2974 bytes
 Allura/development.ini                             |    4 +
 ForgeTracker/forgetracker/model/ticket.py          |   10 +-
 .../forgeuserstats/controllers/userstats.py        |  248 ++++++
 ForgeUserStats/forgeuserstats/main.py              |   66 ++
 .../forgeuserstats/model/.svn/all-wcprops          |   17 +
 ForgeUserStats/forgeuserstats/model/.svn/entries   |   96 +++
 .../model/.svn/text-base/stats.py.svn-base         |  534 ++++++++++++
 ForgeUserStats/forgeuserstats/model/stats.py       |  647 +++++++++++++++
 .../forgeuserstats/templates/.svn/all-wcprops      |   29 +
 .../forgeuserstats/templates/.svn/entries          |  164 ++++
 .../.svn/text-base/artifacts.html.svn-base         |   48 ++
 .../templates/.svn/text-base/commits.html.svn-base |   37 +
 .../templates/.svn/text-base/index.html.svn-base   |  341 ++++++++
 .../templates/.svn/text-base/tickets.html.svn-base |   47 +
 .../forgeuserstats/templates/artifacts.html        |   52 ++
 .../forgeuserstats/templates/commits.html          |   42 +
 ForgeUserStats/forgeuserstats/templates/index.html |  423 ++++++++++
 .../forgeuserstats/templates/tickets.html          |   52 ++
 ForgeUserStats/forgeuserstats/version.py           |    2 +
 ForgeUserStats/setup.py                            |   29 +
 requirements-common.txt                            |    2 +
 35 files changed, 3057 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/controllers/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index 4d7b3ec..f785be2 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -296,6 +296,7 @@ class PostController(BaseController):
             self.post.edit_count = self.post.edit_count + 1
             self.post.last_edit_date = datetime.utcnow()
             self.post.last_edit_by_id = c.user._id
+            self.post.commit()
             g.director.create_activity(c.user, 'modified', self.post,
                     target=self.post.thread.artifact or self.post.thread,
                     related_nodes=[self.post.app_config.project])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 69c80ec..859394b 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -69,6 +69,9 @@ class RootController(WsgiDispatchController):
         if n and not n.url_prefix.startswith('//'):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()
+        for ep in pkg_resources.iter_entry_points("allura.stats"):
+            if ep.name.lower() == 'userstats' and g.show_userstats:
+                setattr(self, ep.name.lower(), ep.load()().root)
         super(RootController, self).__init__()
 
     def _setup_request(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/eventslistener.py
----------------------------------------------------------------------
diff --git a/Allura/allura/eventslistener.py b/Allura/allura/eventslistener.py
new file mode 100644
index 0000000..15adf00
--- /dev/null
+++ b/Allura/allura/eventslistener.py
@@ -0,0 +1,27 @@
+'''This class is supposed to be extended in order to support statistics for
+a specific entity (e.g. user, project, ...). To do so, the new classes should
+overwrite the methods defined here, which will be called when the related
+event happens, so that the statistics for the given entity are updated.'''
+class EventsListener:
+    def newArtifact(self, art_type, art_datetime, project, user):
+        pass
+
+    def modifiedArtifact(self, art_type, art_datetime, project, user):
+        pass
+
+    def newUser(self, user):
+        pass
+
+    def newOrganization(self, organization):
+        pass
+
+    def addUserLogin(self, user):
+        pass
+
+    def newCommit(self, newcommit, project, user):
+        pass
+
+    def ticketEvent(self, event_type, ticket, project, user):
+        pass
+
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/ext/user_profile/templates/user_index.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/templates/user_index.html b/Allura/allura/ext/user_profile/templates/user_index.html
index 2614953..7371eb6 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -236,6 +236,15 @@
     </div>
   </div>
 
+  {% if statslinkurl %}
+    <div class="grid-24">
+      <div class="grid-24" style="margin:0;"><b>User statistics</b></div>
+      <div class="grid-24" style="margin-top:5px;margin-bottom:5px;">
+        <div><a href="{{statslinkurl}}"/>{{statslinkdescription}}</a></div>
+      </div>
+    </div>
+  {% endif %}
+
   {% if c.user.username == user.username %}
       <div class="address-list grid-18">
         <b>Email Addresses</b>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/ext/user_profile/user_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/user_main.py b/Allura/allura/ext/user_profile/user_main.py
index 7754228..759f3f3 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -64,7 +64,14 @@ class UserProfileController(BaseController):
         user = c.project.user_project_of
         if not user:
             raise exc.HTTPNotFound()
-        return dict(user=user)
+        if g.show_userstats:
+            from forgeuserstats.main import ForgeUserStatsApp
+            link, description = ForgeUserStatsApp.createlink(user)
+        else:
+            link, description = None, None
+        return dict(user=user,
+                    statslinkurl = link,
+                    statslinkdescription = description)
     # This will be fully implemented in a future iteration
     # @expose('jinja:allura.ext.user_profile:templates/user_subscriptions.html')
     # def subscriptions(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 14522aa..1e9019c 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -170,6 +170,19 @@ class Globals(object):
         # Zarkov logger
         self._zarkov = None
 
+        self.show_userstats = False
+        # Set listeners to update stats
+        self.statslisteners = []
+        for ep in pkg_resources.iter_entry_points("allura.stats"):
+            if ep.name.lower() == 'userstats':
+                self.show_userstats = config.get(
+                    'user.stats.enable','false')=='true'
+                if self.show_userstats:
+                    self.statslisteners.append(ep.load()().listener)
+            else:
+                self.statslisteners.append(ep.load()().listener)
+
+
     @LazyProperty
     def spam_checker(self):
         """Return a SpamFilter implementation.

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/lib/graphics/__init__.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/graphics/__init__.py b/Allura/allura/lib/graphics/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/lib/graphics/graphic_methods.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/graphics/graphic_methods.py b/Allura/allura/lib/graphics/graphic_methods.py
new file mode 100644
index 0000000..cd3151b
--- /dev/null
+++ b/Allura/allura/lib/graphics/graphic_methods.py
@@ -0,0 +1,69 @@
+from matplotlib.backends.backend_agg import FigureCanvasAgg
+from matplotlib.figure import Figure
+from matplotlib.text import Annotation
+from PIL import Image
+import StringIO
+
+def create_histogram(data, tick_labels, y_label, title):
+    fig = Figure(figsize=(10,5), dpi=80, facecolor='white')
+    ax = fig.add_subplot(111, axisbg='#EEEEFF')
+
+    canvas = FigureCanvasAgg(fig)
+    n, bins, patches = ax.hist(data, facecolor='#330099', edgecolor='white')
+    ax.set_ylabel(y_label)
+    ax.set_title(title)
+
+    ax.set_xticks(range(len(tick_labels)+1))
+    ax.get_xaxis().set_ticklabels(tick_labels, rotation=45, va='top', ha='right')
+    ax.get_xaxis().set_ticks_position('none')
+    ax.set_autoscalex_on(False)
+
+    ax.set_xlim((-1, len(tick_labels)))
+    ax.set_ylim((0, 1+max([data.count(el) for el in data])))
+    fig.subplots_adjust(bottom=0.3)
+
+    canvas.draw()
+        
+    s = canvas.tostring_rgb()
+    l,b,w,h = fig.bbox.bounds
+    w, h = int(w), int(h)
+
+    output = StringIO.StringIO()
+    im = Image.fromstring( "RGB", (w,h), s)
+    im.save(output, 'PNG')
+
+    return output.getvalue()
+
+def create_progress_bar(value):
+    value = value / 100.0
+    if value < 1 / 5.0:
+        color = 'red'
+    elif value < 2 / 5.0:
+        color = 'orange'
+    elif value < 3 / 5.0:
+        color = 'yellow'
+    elif value < 4 / 5.0:
+        color = 'lightgreen'
+    else:
+        color = 'green'
+
+    fig = Figure(figsize=(3,0.5), dpi=40, facecolor='gray')
+    canvas = FigureCanvasAgg(fig)
+    canvas.draw()
+
+    from matplotlib.patches import Rectangle
+    from matplotlib.axes import Axes
+
+    fig.draw_artist(Rectangle((0,0), int(value * 120), 20, color=color))
+    fig.draw_artist(Rectangle((1,0), 119, 19, fill=False, ec='black'))
+
+    l,b,w,h = fig.bbox.bounds
+    s = canvas.tostring_rgb()
+    w, h = int(w), int(h)
+
+    output = StringIO.StringIO()
+    im = Image.fromstring( "RGB", (w,h), s)
+    im.save(output, 'PNG')
+
+    return output.getvalue()
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 4c24a1c..bce0720 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -348,7 +348,7 @@ class VersionedArtifact(Artifact):
 
     version = FieldProperty(S.Int, if_missing=0)
 
-    def commit(self):
+    def commit(self, update_stats=True):
         '''Save off a snapshot of the artifact and increment the version #'''
         self.version += 1
         try:
@@ -373,6 +373,15 @@ class VersionedArtifact(Artifact):
         session(ss).insert_now(ss, state(ss))
         log.info('Snapshot version %s of %s',
                  self.version, self.__class__)
+        if update_stats: 
+            if self.version > 1:
+                for l in g.statslisteners:
+                    l.modifiedArtifact(
+                        self.type_s, self.mod_date, self.project, c.user)
+            else :
+                for l in g.statslisteners:
+                    l.newArtifact(
+                        self.type_s, self.mod_date, self.project, c.user)
         return ss
 
     def get_version(self, n):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 8df4a58..cb9bdfb 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -11,6 +11,7 @@ from hashlib import sha256
 import uuid
 from pytz import timezone
 from datetime import timedelta, date, datetime, time
+from pkg_resources import iter_entry_points
 
 import iso8601
 import pymongo
@@ -35,6 +36,13 @@ from .timeline import ActivityNode, ActivityObject
 
 log = logging.getLogger(__name__)
 
+#This is just to keep the UserStats module completely optional
+has_user_stats_module = False
+for ep in iter_entry_points("allura.stats"):
+    if ep.name.lower() == 'userstats':
+        from forgeuserstats.model.stats import UserStats
+        has_user_stats_module = True
+
 def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
     """
     Returns a bytestring version of 's', encoded as specified in 'encoding'.
@@ -331,6 +339,13 @@ class User(MappedClass, ActivityNode, ActivityObject):
         level = S.OneOf('low', 'high', 'medium'),
         comment=str)])
 
+    #Statistics
+    if has_user_stats_module:
+        stats_id = ForeignIdProperty('UserStats', if_missing=None)
+        stats = RelationProperty('UserStats', via='stats_id')
+    else:
+        stats_id = FieldProperty(S.ObjectId, if_missing=None)
+
     @property
     def activity_name(self):
         return self.display_name or self.username
@@ -577,6 +592,9 @@ class User(MappedClass, ActivityNode, ActivityObject):
         user = auth_provider.register_user(doc)
         if user and 'display_name' in doc:
             user.set_pref('display_name', doc['display_name'])
+        if user:
+            for l in g.statslisteners:
+                l.newUser(user)
         if user and make_project:
             n = M.Neighborhood.query.get(name='Users')
             n.register_project(auth_provider.user_project_shortname(user),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 9d26242..1795b1b 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -203,7 +203,7 @@ class Thread(Artifact, ActivityObject):
     def add_post(self, **kw):
         """Helper function to avoid code duplication."""
         p = self.post(**kw)
-        p.commit()
+        p.commit(update_stats=False)
         self.num_replies += 1
         if not self.first_post:
             self.first_post_id = p._id

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 539df2a..5e4f21f 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -110,12 +110,21 @@ def refresh_repo(repo, all_commits=False, notify=True):
             if (i+1) % 100 == 0:
                 log.info('Compute last commit info %d: %s', (i+1), ci._id)
 
+    for commit in commit_ids:
+        new = repo.commit(commit)
+        user = User.by_email_address(new.committed.email)
+        if user is None:
+            user = User.by_username(new.committed.name)
+        if user is not None:
+            for l in g.statslisteners:
+                l.newCommit(new, repo.app_config.project, user)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event(
             'repo_refreshed',
             commit_number=len(commit_ids),
             new=bool(new_commit_ids))
+
     # Send notifications
     if notify:
         send_notifications(repo, commit_ids)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/public/nf/images/down.png
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/images/down.png b/Allura/allura/public/nf/images/down.png
new file mode 100644
index 0000000..7ecfe70
Binary files /dev/null and b/Allura/allura/public/nf/images/down.png differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/public/nf/images/equal.png
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/images/equal.png b/Allura/allura/public/nf/images/equal.png
new file mode 100644
index 0000000..c08136a
Binary files /dev/null and b/Allura/allura/public/nf/images/equal.png differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/allura/public/nf/images/up.png
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/images/up.png b/Allura/allura/public/nf/images/up.png
new file mode 100644
index 0000000..f044a67
Binary files /dev/null and b/Allura/allura/public/nf/images/up.png differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 3dc76d0..7d2bbe0 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -126,6 +126,10 @@ scm.repos.tarball.url_prefix = http://localhost/
 
 trovecategories.enableediting = true
 
+# If set to false, the stats of the user are not
+# updated and they are not shown to users.
+user.stats.enable = true
+
 # ActivityStream
 activitystream.master = mongodb://127.0.0.1:27017
 activitystream.database = activitystream

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index dbfda81..c2b15c7 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -435,6 +435,8 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
                 ('Status', old.status, self.status) ]
             if old.status != self.status and self.status in c.app.globals.set_of_closed_status_names:
                 h.log_action(log, 'closed').info('')
+                for l in g.statslisteners:
+                    l.ticketEvent("closed", self, self.project, self.assigned_to)
             for key in self.custom_fields:
                 fields.append((key, old.custom_fields.get(key, ''), self.custom_fields[key]))
             for title, o, n in fields:
@@ -447,6 +449,9 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
                 changes.append('Owner updated: %r => %r' % (
                         o and o.username, n and n.username))
                 self.subscribe(user=n)
+                for l in g.statslisteners :
+                    l.ticketEvent("assigned", self, self.project, n)
+                    if o: l.ticketEvent("revoked", self, self.project, o)
             if old.description != self.description:
                 changes.append('Description updated:')
                 changes.append('\n'.join(
@@ -459,7 +464,10 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
         else:
             self.subscribe()
             if self.assigned_to_id:
-                self.subscribe(user=User.query.get(_id=self.assigned_to_id))
+                user = User.query.get(_id=self.assigned_to_id)
+                for l in g.statslisteners :
+                    l.ticketEvent("assigned", self, self.project, user)
+                self.subscribe(user=user)
             description = ''
             subject = self.email_subject
             Thread.new(discussion_id=self.app_config.discussion_id,

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/__init__.py b/ForgeUserStats/forgeuserstats/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/controllers/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/__init__.py b/ForgeUserStats/forgeuserstats/controllers/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
new file mode 100644
index 0000000..2bfaf82
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -0,0 +1,248 @@
+from tg import expose
+from tg.decorators import with_trailing_slash
+from datetime import datetime
+from allura.controllers import BaseController
+import allura.model as M
+from allura.lib.graphics.graphic_methods import create_histogram, create_progress_bar
+from forgeuserstats.model.stats import UserStats
+
+class ForgeUserStatsController(BaseController):
+
+    @expose()
+    def _lookup(self, part, *remainder):
+        user = M.User.query.get(username=part)
+
+        if not self.user:
+            return ForgeUserStatsController(user=user), remainder
+        if part == "category":
+            return ForgeUserStatsCatController(self.user, self.stats, None), remainder
+        if part == "metric":
+            return ForgeUserStatsMetricController(self.user, self.stats), remainder
+
+    def __init__(self, user=None):
+        self.user = user
+        if self.user:
+            self.stats = self.user.stats
+            if not self.stats:
+                self.stats = UserStats.create(self.user)
+
+        super(ForgeUserStatsController, self).__init__()
+
+    @expose('jinja:forgeuserstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        if not self.user: 
+            return dict(user=None)
+        stats = self.stats
+
+        ret_dict = _getDataForCategory(None, stats)
+        ret_dict['user'] = self.user
+
+        ret_dict['registration_date'] = stats.registration_date
+
+        ret_dict['totlogins'] = stats.tot_logins_count
+        ret_dict['last_login'] = stats.last_login
+        if stats.last_login:
+            ret_dict['last_login_days'] = \
+                (datetime.utcnow()-stats.last_login).days
+
+        categories = {}
+        for p in self.user.my_projects():
+            for cat in p.trove_topic:
+                cat = M.TroveCategory.query.get(_id = cat)
+                if categories.get(cat):
+                    categories[cat] += 1
+                else:
+                    categories[cat] = 1
+        categories = sorted(categories.items(), key=lambda (x,y): y,reverse=True)
+
+        ret_dict['lastmonth_logins'] = stats.getLastMonthLogins()
+        ret_dict['categories'] = categories
+        days = ret_dict['days']
+        if days >= 30: 
+            ret_dict['permonthlogins'] = \
+                round(stats.tot_logins_count*30.0/days,2)
+        else:
+            ret_dict['permonthlogins'] = 'n/a'
+
+        ret_dict['codepercentage'] = stats.codeRanking()
+        ret_dict['discussionpercentage'] = stats.discussionRanking()
+        ret_dict['ticketspercentage'] = stats.ticketsRanking()
+        ret_dict['codecontribution'] = stats.getCodeContribution()
+        ret_dict['discussioncontribution'] = stats.getDiscussionContribution()
+        ret_dict['ticketcontribution'] = stats.getTicketsContribution()
+        ret_dict['maxcodecontrib'], ret_dict['averagecodecontrib'] =\
+            stats.getMaxAndAverageCodeContribution()
+        ret_dict['maxdisccontrib'], ret_dict['averagedisccontrib'] =\
+            stats.getMaxAndAverageDiscussionContribution()
+        ret_dict['maxticketcontrib'], ret_dict['averageticketcontrib'] =\
+            stats.getMaxAndAverageTicketsSolvingPercentage()
+
+        return ret_dict
+
+    @expose()
+    def categories_graph(self):
+        categories = {}
+        for p in self.user.my_projects():
+            for cat in p.trove_topic:
+                cat = M.TroveCategory.query.get(_id = cat)
+                if categories.get(cat):
+                    categories[cat] += 1
+                else:
+                    categories[cat] = 1
+        data = []
+        labels = []
+        i = 0
+        for cat in sorted(categories.keys(), key=lambda x:x.fullname):
+            n = categories[cat]
+            data = data + [i] * n
+            label = cat.fullname
+            if len(label) > 15:
+                label = label[:15] + "..."
+            labels.append(label)
+            i += 1
+
+        return create_histogram(data, labels, 
+            'Number of projects', 'Projects by category')
+
+    @expose()
+    def code_ranking_bar(self):
+        return create_progress_bar(self.stats.codeRanking())
+
+    @expose()
+    def discussion_ranking_bar(self):
+        return create_progress_bar(self.stats.discussionRanking())
+
+    @expose()
+    def tickets_ranking_bar(self):
+        return create_progress_bar(self.stats.ticketsRanking())
+
+class ForgeUserStatsCatController(BaseController):
+    @expose()
+    def _lookup(self, category, *remainder):
+        cat = M.TroveCategory.query.get(fullname=category)
+        return ForgeUserStatsCatController(self.user, cat), remainder
+
+    def __init__(self, user, stats, category):
+        self.user = user
+        self.stats = stats
+        self.category = category
+        super(ForgeUserStatsCatController, self).__init__()
+
+    @expose('jinja:forgeuserstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        if not self.user:
+            return dict(user=None)
+        stats = self.stats
+        
+        cat_id = None
+        if self.category: 
+            cat_id = self.category._id
+        ret_dict = _getDataForCategory(cat_id, stats)
+        ret_dict['user'] = self.user
+        ret_dict['registration_date'] = stats.registration_date
+        ret_dict['category'] = self.category
+        
+        return ret_dict
+
+class ForgeUserStatsMetricController(BaseController):
+
+    def __init__(self, user, stats):
+        self.user = user
+        self.stats = stats
+        super(ForgeUserStatsMetricController, self).__init__()
+
+    @expose('jinja:forgeuserstats:templates/commits.html')
+    @with_trailing_slash
+    def commits(self, **kw):
+        if not self.user:
+            return dict(user=None)
+        stats = self.stats
+        
+        commits = stats.getCommitsByCategory()
+        return dict(user = self.user,
+                    data = commits) 
+
+    @expose('jinja:forgeuserstats:templates/artifacts.html')
+    @with_trailing_slash
+    def artifacts(self, **kw):
+        if not self.user:
+            return dict(user=None)
+
+        stats = self.stats       
+        artifacts = stats.getArtifactsByCategory(detailed=True)
+        return dict(user = self.user, data = artifacts) 
+
+    @expose('jinja:forgeuserstats:templates/tickets.html')
+    @with_trailing_slash
+    def tickets(self, **kw):
+        if not self.user: 
+            return dict(user=None)
+
+        artifacts = self.stats.getTicketsByCategory()
+        return dict(user = self.user, data = artifacts) 
+
+def _getDataForCategory(category, stats):
+    totcommits = stats.getCommits(category)
+    tottickets = stats.getTickets(category)
+    averagetime = tottickets.get('averagesolvingtime')
+    artifacts_by_type = stats.getArtifactsByType(category)
+    totartifacts = artifacts_by_type.get(None) 
+    if totartifacts:
+        del artifacts_by_type[None]
+    else:
+        totartifacts = dict(created=0, modified=0)
+    lmcommits = stats.getLastMonthCommits(category)
+    lm_artifacts_by_type = stats.getLastMonthArtifactsByType(category)
+    lm_totartifacts = stats.getLastMonthArtifacts(category)
+    lm_tickets = stats.getLastMonthTickets(category)
+
+    averagetime = lm_tickets.get('averagesolvingtime')
+
+    days = (datetime.utcnow() - stats.registration_date).days
+    if days >= 30: 
+        pmartifacts = dict(
+            created = round(totartifacts['created']*30.0/days,2),
+            modified=round(totartifacts['modified']*30.0/days,2))
+        pmcommits = dict(
+            number=round(totcommits['number']*30.0/days,2),
+            lines=round(totcommits['lines']*30.0/days,2))
+        pmtickets = dict(
+            assigned=round(tottickets['assigned']*30.0/days,2),
+            revoked=round(tottickets['revoked']*30.0/days,2),
+            solved=round(tottickets['solved']*30.0/days,2),
+            averagesolvingtime='n/a')
+        for key in artifacts_by_type:
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = \
+                round(value['created']*30.0/days,2)
+            artifacts_by_type[key]['pmmodified']= \
+                round(value['modified']*30.0/days,2)
+    else: 
+        pmartifacts = dict(created='n/a', modified='n/a')
+        pmcommits = dict(number='n/a', lines='n/a')
+        pmtickets = dict(
+            assigned='n/a',
+            revoked='n/a',
+            solved='n/a',
+            averagesolvingtime='n/a')
+        for key in artifacts_by_type:
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = 'n/a'
+            artifacts_by_type[key]['pmmodified']= 'n/a'
+
+    return dict(
+        days = days,
+        totcommits = totcommits,
+        lastmonthcommits = lmcommits,
+        lastmonthtickets = lm_tickets,
+        tottickets = tottickets,
+        permonthcommits = pmcommits,
+        totartifacts = totartifacts,
+        lastmonthartifacts = lm_totartifacts,
+        permonthartifacts = pmartifacts,
+        artifacts_by_type = artifacts_by_type,
+        lastmonth_artifacts_by_type = lm_artifacts_by_type,
+        permonthtickets = pmtickets)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
new file mode 100644
index 0000000..43ca2f3
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -0,0 +1,66 @@
+import logging
+from datetime import datetime
+
+from allura.eventslistener import EventsListener
+from model.stats import UserStats
+from controllers.userstats import ForgeUserStatsController
+
+log = logging.getLogger(__name__)
+
+class UserStatsListener(EventsListener):
+    def newArtifact(self, art_type, art_datetime, project, user):
+        stats = user.stats
+        if not stats:
+            stats = UserStats.create(user)
+        stats.addNewArtifact(art_type, art_datetime, project)
+
+    def modifiedArtifact(self, art_type, art_datetime, project, user):
+        stats = user.stats
+        if not stats:
+            stats = UserStats.create(user)
+
+        stats.addModifiedArtifact(art_type, art_datetime, project)
+
+    def newUser(self, user):
+        stats = UserStats.create(user)
+
+    def ticketEvent(self, event_type, ticket, project, user):
+        if user is None:
+            return
+        stats = user.stats
+        if not stats:
+            stats = UserStats.create(user)
+
+        if event_type == "assigned": 
+            stats.addAssignedTicket(ticket, project)
+        elif event_type == "revoked":
+            stats.addRevokedTicket(ticket, project)
+        elif event_type == "closed":
+            stats.addClosedTicket(ticket, project)
+
+    def newCommit(self, newcommit, project, user):
+        stats = user.stats
+        if not stats:
+            stats = UserStats.create(user)
+
+        stats.addCommit(newcommit, project)
+
+    def addUserLogin(self, user):
+        stats = user.stats
+        if not stats:
+            stats = UserStats.create(user)
+
+        stats.addLogin()
+
+    def newOrganization(self, organization):
+        pass
+
+class ForgeUserStatsApp:
+    root = ForgeUserStatsController()
+    listener = UserStatsListener()
+
+    @classmethod
+    def createlink(cls, user):
+        return (
+            "/userstats/%s/" % user.username, 
+            "%s personal statistcs" % user.display_name)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops b/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
new file mode 100644
index 0000000..a5d5661
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/model/.svn/all-wcprops
@@ -0,0 +1,17 @@
+K 25
+svn:wc:ra_dav:version-url
+V 58
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model
+END
+stats.py
+K 25
+svn:wc:ra_dav:version-url
+V 67
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model/stats.py
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 70
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/model/__init__.py
+END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/entries b/ForgeUserStats/forgeuserstats/model/.svn/entries
new file mode 100644
index 0000000..c26dfd9
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/model/.svn/entries
@@ -0,0 +1,96 @@
+10
+
+dir
+4
+https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats/model
+https://xp-dev.com/svn/allura
+
+
+
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+46ed536d-f66c-413e-a53e-834384f708db
+
+stats.py
+file
+
+
+
+
+2012-11-05T14:43:25.729756Z
+21591047edf4fabfb1b70150af5bd0c2
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+23647
+
+__init__.py
+file
+
+
+
+
+2012-11-05T14:43:25.729756Z
+d41d8cd98f00b204e9800998ecf8427e
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+0
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base b/ForgeUserStats/forgeuserstats/model/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base b/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
new file mode 100644
index 0000000..f434e4e
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/model/.svn/text-base/stats.py.svn-base
@@ -0,0 +1,534 @@
+import pymongo
+from pylons import c, g, request
+
+import bson
+from ming import schema as S
+from ming import Field, Index, collection
+from ming.orm import session, state, Mapper
+from ming.orm import FieldProperty, RelationProperty, ForeignIdProperty
+from ming.orm.declarative import MappedClass
+from datetime import datetime, timedelta
+import difflib
+
+from allura.model.session import main_orm_session, main_doc_session
+from allura.model.session import project_orm_session
+from allura.model import User
+import allura.model as M
+from allura.lib import helpers as h
+
+class UserStats(MappedClass):
+    SALT_LEN=8
+    class __mongometa__:
+        name='userstats'
+        session = main_orm_session
+        unique_indexes = [ 'userid' ]
+
+    _id=FieldProperty(S.ObjectId)
+    userid = ForeignIdProperty('User')
+
+    registration_date = FieldProperty(datetime)
+    tot_logins_count = FieldProperty(int, if_missing = 0)
+    last_login = FieldProperty(datetime)
+    general = FieldProperty([dict(category = S.ObjectId,
+                                  messages = [dict(messagetype = str,
+                                                   created = int,
+                                                   modified = int)],
+                                  tickets = dict(solved = int,
+                                                 assigned = int,
+                                                 revoked = int,
+                                                 totsolvingtime = int),
+                                  commits = [dict(lines = int,
+                                                  number = int,
+                                                  language = S.ObjectId)])])
+
+    lastmonth= FieldProperty(dict(logins=[datetime],
+                                  messages=[dict(datetime=datetime,
+                                                 created=bool,
+                                                 categories=[S.ObjectId],
+                                                 messagetype=str)],
+                                  assignedtickets=[dict(datetime=datetime,
+                                                        categories=[S.ObjectId])],
+                                  revokedtickets=[dict(datetime=datetime,
+                                                       categories=[S.ObjectId])],
+                                  solvedtickets=[dict(datetime=datetime,
+                                                      categories=[S.ObjectId],
+                                                      solvingtime=int)],
+                                  commits=[dict(datetime=datetime,
+                                                categories=[S.ObjectId],
+                                                programming_languages=[S.ObjectId],
+                                                lines=int)]))
+    reluser = RelationProperty('User')
+
+
+    def codeRanking(self) :
+        def _getCodeContribution(stats) :
+            for val in stats['general'] :
+                if val['category'] is None :
+                    for commits in val['commits'] :
+                         if commits['language'] is None : 
+                             return (commits.lines, commits.number)
+            return (0,0) 
+
+        lst = list(self.query.find())
+        totn = len(lst)
+        codcontr = _getCodeContribution(self)
+        upper = len([x for x in lst if _getCodeContribution(x) > codcontr])
+        percentage = upper * 100.0 / totn
+        if percentage < 1 / 6.0 : return 5
+        if percentage < 2 / 6.0 : return 4
+        if percentage < 3 / 6.0 : return 3
+        if percentage < 4 / 6.0 : return 2
+        if percentage < 5 / 6.0 : return 1
+        return 0
+
+    def discussionRanking(self) :
+        def _getDiscussionContribution(stats) :
+            for val in stats['general'] :
+                if val['category'] is None :
+                    for artifact in val['messages'] :
+                         if artifact['messagetype'] is None : 
+                             return artifact.created + artifact.modified
+            return 0
+
+        lst = list(self.query.find())
+        totn = len(lst)
+        disccontr = _getDiscussionContribution(self)
+        upper = len([x for x in lst if _getDiscussionContribution(x) > disccontr])
+        percentage = upper * 100.0 / totn
+        if percentage < 1 / 6.0 : return 5
+        if percentage < 2 / 6.0 : return 4
+        if percentage < 3 / 6.0 : return 3
+        if percentage < 4 / 6.0 : return 2
+        if percentage < 5 / 6.0 : return 1
+        return 0
+
+    def ticketsRanking(self) :
+
+        def _getTicketsPercentage(stats) :
+            for val in stats['general'] :
+                if val['category'] is None :
+                    if val['tickets']['assigned'] == 0 : percentage = 0
+                    else :
+                        percentage = val['tickets']['solved'] \
+                                    / val['tickets']['assigned']
+            return 0
+
+        percentage = _getTicketsPercentage(self)
+        if percentage > 1 / 6.0 : return 5
+        if percentage > 2 / 6.0 : return 4
+        if percentage > 3 / 6.0 : return 3
+        if percentage > 4 / 6.0 : return 2
+        if percentage > 5 / 6.0 : return 1
+        return 0
+
+    def getCommits(self, category = None) :
+        i = getElementIndex(self.general, category = category)
+        if i is None : return {'number' : 0, 'lines': 0}
+        cat = self.general[i]
+        j = getElementIndex(cat.commits, language = None)
+        if j is None : return {'number' : 0, 'lines': 0}
+        return {'number': cat.commits[j]['number'], 
+                'lines' : cat.commits[j]['lines']}
+
+    def getArtifacts(self, category = None, art_type = None) :
+        i = getElementIndex(self.general, category = category)
+        if i is None : return {'created' : 0, 'modified': 0}
+        cat = self.general[i]
+        j = getElementIndex(cat.messages, art_type = art_type)
+        if j is None : return {'created' : 0, 'modified': 0}
+        return {'created'  : cat[j].created, 
+                'modified' : cat[j].modified}
+
+    def getTickets(self, category = None) :
+        i = getElementIndex(self.general, category = category)
+        if i is None : return {'assigned'           : 0,
+                               'solved'             : 0,
+                               'revoked'            : 0,
+                               'averagesolvingtime' : None}
+        if self.general[i].tickets.solved > 0 :
+           tot = self.general[i].tickets.totsolvingtime 
+           number = self.general[i].tickets.solved
+           average = tot / number
+
+        else : average = None
+        return {'assigned'           : self.general[i].tickets.assigned,
+                'solved'             : self.general[i].tickets.solved,
+                'revoked'            : self.general[i].tickets.revoked,
+                'averagesolvingtime' : _convertTimeDiff(average)}
+
+    def getCommitsByCategory(self) :
+        by_cat = {}
+        for entry in self.general :
+            cat = entry.category
+            i = getElementIndex(entry.commits, language = None)
+            if i is None : n, lines = 0, 0
+            else : n, lines = entry.commits[i].number, entry.commits[i].lines
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            by_cat[cat] = {'number' : n, 'lines' : lines}
+        return by_cat
+
+    def getCommitsByLanguage(self) :
+        langlist = []
+        by_lang = {}
+        i = getElementIndex(self.general, category=None)
+        if i is None : return {'number' : 0, 'lines' : 0}
+        return dict([(el.language, {'lines' : el.lines, 'number':el.number})
+                     for el in self.general[i].commits])
+
+    def getArtifactsByCategory(self, detailed=False) :
+        by_cat = {}
+        for entry in self.general :
+            cat = entry.category
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            if detailed : 
+                by_cat[cat] = entry.messages
+            else : 
+                i = getElementIndex(entry.messages, messagetype=None)
+                if i is not None : by_cat[cat] = entry.messages[i]
+                else : by_cat[cat] = {'created' : 0, 'modified' : 0}
+        return by_cat
+
+    def getArtifactsByType(self, category=None) :
+        i = getElementIndex(self.general, category = category)
+        if i is None : return {}
+        entry = self.general[i].messages
+        by_type = dict([(el.messagetype, {'created' : el.created, 
+                                          'modified': el.modified})
+                         for el in entry])
+        return by_type
+
+    def getTicketsByCategory(self) :
+        by_cat = {}
+        for entry in self.general :
+            cat = entry.category
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            a, s = entry.tickets.assigned, entry.tickets.solved
+            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
+            if s : average = time / s
+            else : average = None
+            by_cat[cat] = {'assigned'           : a,
+                           'solved'             : s,
+                           'revoked'            : r, 
+                           'averagesolvingtime' : _convertTimeDiff(average)}
+        return by_cat
+
+    def getLastMonthCommits(self, category = None) :
+        self.checkOldArtifacts() 
+        lineslist = [el.lines for el in self.lastmonth.commits
+                     if category in el.categories + [None]]
+        return {'number': len(lineslist), 'lines':sum(lineslist)}
+
+    def getLastMonthCommitsByCategory(self) :
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist :
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if cat in el.categories + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            by_cat[cat] = {'number' : n, 'lines' : lines}
+        return by_cat
+
+    def getLastMonthCommitsByLanguage(self) :
+        self.checkOldArtifacts() 
+        seen = set()
+        langlist=[el.language for el in self.general
+                  if el.language not in seen and not seen.add(el.language)]
+
+        by_lang = {}
+        for lang in langlist :
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if lang in el.programming_languages + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if lang != None : lang = M.TroveCategory.query.get(_id = lang)
+            by_lang[lang] = {'number' : n, 'lines' : lines}
+        return by_lang
+
+    def getLastMonthArtifacts(self, category = None) :
+        self.checkOldArtifacts() 
+        cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
+                                     for el in self.lastmonth.messages
+                                     if category is None or 
+                                        category in el.categories], (0,0))
+        return {'created': cre, 'modified' : mod}
+
+    def getLastMonthArtifactsByType(self, category = None) :
+        self.checkOldArtifacts() 
+        seen = set()
+        types=[el.messagetype for el in self.lastmonth.messages
+               if el.messagetype not in seen and not seen.add(el.messagetype)]
+
+        by_type = {}
+        for t in types :
+            cre, mod = reduce(addtuple, 
+                              [(int(el.created),1-int(el.created))
+                               for el in self.lastmonth.messages
+                               if el.messagetype == t and
+                                  category in [None]+el.categories],
+                              (0,0))
+            by_type[t] = {'created': cre, 'modified' : mod}
+        return by_type
+
+    def getLastMonthArtifactsByCategory(self) :
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist :
+            cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
+                                         for el in self.lastmonth.messages 
+                                         if cat in el.categories + [None]], (0,0))
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            by_cat[cat] = {'created' : cre, 'modified' : mod}
+        return by_cat
+
+    def getLastMonthTickets(self, category = None) :
+        self.checkOldArtifacts()
+        a = len([el for el in self.lastmonth.assignedtickets
+                 if category in el.categories + [None]])
+        r = len([el for el in self.lastmonth.revokedtickets
+                 if category in el.categories + [None]])
+        s, time = reduce(addtuple, 
+                         [(1, el.solvingtime)
+                          for el in self.lastmonth.solvedtickets
+                          if category in el.categories + [None]],
+                         (0,0))
+        if category!=None : category = M.TroveCategory.query.get(_id=category)
+        if s > 0 : time = time / s
+        else : time = None
+        return {'assigned'           : a,
+                'revoked'            : r,
+                'solved'             : s, 
+                'averagesolvingtime' : _convertTimeDiff(time)}
+        
+    def getLastMonthTicketsByCategory(self) :
+        self.checkOldArtifacts()
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+        by_cat = {}
+        for cat in catlist :
+            a = len([el for el in self.lastmonth.assignedtickets
+                     if cat in el.categories + [None]])
+            r = len([el for el in self.lastmonth.revokedtickets
+                     if cat in el.categories + [None]])
+            s, time = reduce(addtuple, [(1, el.solvingtime)
+                                        for el in self.lastmonth.solvedtickets
+                                        if cat in el.categories + [None]],(0,0))
+            if cat != None : cat = M.TroveCategory.query.get(_id = cat)
+            if s > 0 : time = time / s
+            else : time = None
+            by_cat[cat] = {'assigned'           : a,
+                           'revoked'            : r,
+                           'solved'             : s, 
+                           'averagesolvingtime' : _convertTimeDiff(time)}
+        return by_cat
+        
+    def getLastMonthLogins(self) :
+        self.checkOldArtifacts()
+        return len(self.lastmonth.logins)
+
+    def checkOldArtifacts(self) :
+        now = datetime.now()
+        for m in self.lastmonth.messages :
+            if now - m.datetime > timedelta(30) :
+               self.lastmonth.messages.remove(m)
+        for t in self.lastmonth.assignedtickets :
+            if now - t.datetime > timedelta(30) :
+               self.lastmonth.assignedtickets.remove(t)
+        for t in self.lastmonth.revokedtickets :
+            if now - t.datetime > timedelta(30) :
+               self.lastmonth.revokedtickets.remove(t)
+        for t in self.lastmonth.solvedtickets :
+            if now - t.datetime > timedelta(30) :
+               self.lastmonth.solvedtickets.remove(t)
+
+    def addNewArtifact(self, art_type, art_datetime, project) :
+        self._updateArtifactsStats(art_type, art_datetime, project, "created")
+
+    def addModifiedArtifact(self, art_type, art_datetime, project) :
+        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
+
+    def addAssignedTicket(self, ticket, project) :
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'assigned')
+        self.lastmonth.assignedtickets.append({'datetime'   : ticket.mod_date,
+                                               'categories' : topics})
+
+    def addRevokedTicket(self, ticket, project) :
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'revoked')
+        self.lastmonth.revokedtickets.append({'datetime'   : ticket.mod_date,
+                                              'categories' : topics})
+        self.checkOldArtifacts()
+
+    def addClosedTicket(self, ticket, project) :
+        topics = [t for t in project.trove_topic if t]
+        s_time=int((datetime.utcnow()-ticket.created_date).total_seconds())
+        self._updateTicketsStats(topics, 'solved', s_time = s_time)
+        self.lastmonth.solvedtickets.append({'datetime'   : ticket.mod_date,
+                                             'categories' : topics,
+                                             'solvingtime': s_time})
+        self.checkOldArtifacts()
+
+    def addCommit(self, newcommit, project) :
+        def _addCommitData(stats, topics, languages, newblob, oldblob = None) :
+            if oldblob : listold = list(oldblob)
+            else : listold = []
+            listnew = list(newblob)
+
+            if oldblob is None : lines = len(listnew)
+            elif newblob.has_html_view :
+                diff = difflib.unified_diff(listold, listnew,
+                         ('old' + oldblob.path()).encode('utf-8'),
+                         ('new' + newblob.path()).encode('utf-8'))
+                lines = len([l for l in diff if len(l) > 0 and l[0] == '+']) - 1
+            else : lines = 0
+            
+            lt = topics + [None]
+            ll = languages + [None]
+            for t in lt :
+                i = getElementIndex(stats.general, category=t) 
+                if i is None :
+                    newstats = {'category' : t,
+                                'commits'  : [],
+                                'tickets'  : {'assigned'       : 0,
+                                              'solved'         : 0,
+                                              'revoked'        : 0,
+                                              'totsolvingtime' : 0},
+                                'messages' : []}   
+                    stats.general.append(newstats)
+                    i = getElementIndex(stats.general, category=t)
+                for lang in ll :
+                    j = getElementIndex(stats.general[i]['commits'], 
+                                        language=lang)
+                    if j is None :
+                        stats.general[i]['commits'].append({'language': lang,
+                                                            'lines'   : lines,
+                                                            'number'  : 1})
+                    else :
+                        stats.general[i]['commits'][j].lines += lines
+                        stats.general[i]['commits'][j].number += 1
+            return lines
+
+        topics = [t for t in project.trove_topic if t]
+        languages = [l for l in project.trove_language if l]
+        now = datetime.utcnow()
+
+        d = newcommit.diffs
+        if len(newcommit.parent_ids) > 0 :
+            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
+
+        totlines = 0
+        for changed in d.changed :
+            newblob = newcommit.tree.get_blob_by_path(changed)
+            oldblob = oldcommit.tree.get_blob_by_path(changed)
+            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+
+        for copied in d.copied :
+            newblob = newcommit.tree.get_blob_by_path(copied['new'])
+            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
+            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+
+        for added in d.added :
+            newblob = newcommit.tree.get_blob_by_path(added)
+            totlines+=_addCommitData(self, topics, languages, newblob)
+
+        self.lastmonth.commits.append({'datetime' : now,
+                                       'categories' : topics,
+                                       'programming_languages' : languages,
+                                       'lines' : totlines})
+        self.checkOldArtifacts()
+
+    def addLogin(self) :
+        now = datetime.utcnow()
+        self.last_login = now
+        self.tot_logins_count += 1
+        self.lastmonth.logins.append(now)
+        self.checkOldArtifacts()
+        
+    def _updateArtifactsStats(self, art_type, art_datetime, project, action) :
+        if action not in ['created', 'modified'] : return
+        topics = [t for t in project.trove_topic if t]
+        lt = [None] + topics
+        for mtype in [None, art_type] :
+            for t in lt :
+                i = getElementIndex(self.general, category = t)
+                if i is None :
+                    msg = {'category' : t,
+                           'commits'  : [],
+                           'tickets'  : {'solved'         : 0,
+                                         'assigned'       : 0,
+                                         'revoked'        : 0,
+                                         'totsolvingtime' : 0},
+                           'messages' : []}
+                    self.general.append(msg)
+                    i = getElementIndex(self.general, category = t)
+                j = getElementIndex(self.general[i]['messages'], messagetype = mtype)
+                if j is None : 
+                    entry = {'messagetype' : mtype,
+                             'created'     : 0,
+                             'modified'    : 0}
+                    entry[action] += 1
+                    self.general[i]['messages'].append(entry)
+                else : self.general[i]['messages'][j][action] += 1
+
+        self.lastmonth.messages.append({'datetime'   : art_datetime,
+                                        'created'    : action == 'created',
+                                        'categories' : topics,
+                                        'messagetype': art_type})
+        self.checkOldArtifacts() 
+
+    def _updateTicketsStats(self, topics, action, s_time = None) :
+        if action not in ['solved', 'assigned', 'revoked'] : return
+        lt = topics + [None]
+        for t in lt :
+            i = getElementIndex(self.general, category = t)
+            if i is None :
+                stats = {'category' : t,
+                         'commits'  : [],
+                         'tickets'  : {'solved'         : 0,
+                                       'assigned'       : 0,
+                                       'revoked'        : 0,
+                                       'totsolvingtime' : 0},
+                         'messages' : [] }
+                self.general.append(stats)
+                i = getElementIndex(self.general, category = t)
+            self.general[i]['tickets'][action] += 1 
+            if action == 'solved' : 
+                self.general[i]['tickets']['totsolvingtime']+=s_time
+
+def getElementIndex(el_list, **kw) :
+    for i in range(len(el_list)) :
+        for k in kw : 
+            if el_list[i].get(k) != kw[k] : break
+        else : return i
+    return None
+
+def addtuple(l1, l2) :
+    a, b = l1
+    x, y = l2
+    return (a+x, b+y)
+
+def _convertTimeDiff(int_seconds) :
+    if int_seconds is None : return None
+    diff = timedelta(seconds = int_seconds)
+    days, seconds = diff.days, diff.seconds
+    hours = seconds / 3600
+    seconds = seconds % 3600
+    minutes = seconds / 60
+    seconds = seconds % 60
+    return {'days'    : days, 
+            'hours'   : hours, 
+            'minutes' : minutes,
+            'seconds' : seconds}
+
+Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/__init__.py b/ForgeUserStats/forgeuserstats/model/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
new file mode 100644
index 0000000..6e9d063
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -0,0 +1,647 @@
+import pymongo
+from pylons import c, g, request
+
+import bson
+from ming import schema as S
+from ming import Field, Index, collection
+from ming.orm import session, state, Mapper
+from ming.orm import FieldProperty, RelationProperty, ForeignIdProperty
+from ming.orm.declarative import MappedClass
+from datetime import datetime, timedelta
+import difflib
+
+from allura.model.session import main_orm_session
+from allura.lib import helpers as h
+
+class UserStats(MappedClass):
+    class __mongometa__:
+        name='userstats'
+        session = main_orm_session
+        unique_indexes = [ '_id', 'user_id']
+
+    _id=FieldProperty(S.ObjectId)
+
+    registration_date = FieldProperty(datetime)
+    tot_logins_count = FieldProperty(int, if_missing = 0)
+    last_login = FieldProperty(datetime)
+    general = FieldProperty([dict(
+        category = S.ObjectId,
+        messages = [dict(
+            messagetype = str,
+            created = int,
+            modified = int)],
+        tickets = dict(
+            solved = int,
+            assigned = int,
+            revoked = int,
+            totsolvingtime = int),
+        commits = [dict(
+            lines = int,
+            number = int,
+            language = S.ObjectId)])])
+
+    lastmonth=FieldProperty(dict(
+        logins=[datetime],
+            messages=[dict(
+            datetime=datetime,
+            created=bool,
+            categories=[S.ObjectId],
+            messagetype=str)],
+        assignedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId])],
+        revokedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId])],
+        solvedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId],
+            solvingtime=int)],
+        commits=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId],
+            programming_languages=[S.ObjectId],
+            lines=int)]))
+    user_id = FieldProperty(S.ObjectId)
+
+    @classmethod
+    def create(cls, user):
+        stats = cls.query.get(user_id = user._id)
+        if stats:
+            return stats
+        stats = cls(user_id=user._id,
+            registration_date = datetime.utcnow())
+        user.stats_id = stats._id
+        session(stats).flush(stats)
+        session(user).flush(user)
+        return stats
+
+    def getCodeContribution(self):
+        days=(datetime.today() - self.registration_date).days
+        if not days:
+            days=1
+        for val in self['general']:
+            if val['category'] is None:
+                for commits in val['commits']:
+                    if commits['language'] is None: 
+                        if days > 30:
+                            return round(float(commits.lines)/days*30, 2)
+                        else:
+                            return float(commits.lines)
+        return 0
+
+    def getDiscussionContribution(self):
+        days=(datetime.today() - self.registration_date).days
+        if not days:
+            days=1
+        for val in self['general']:
+            if val['category'] is None:
+                for artifact in val['messages']:
+                    if artifact['messagetype'] is None: 
+                        tot = artifact.created+artifact.modified
+                        if days > 30:
+                            return round(float(tot)/days*30,2)
+                        else:
+                            return float(tot)
+        return 0
+
+    def getTicketsContribution(self):
+        for val in self['general']:
+            if val['category'] is None:
+                tickets = val['tickets']
+                if tickets.assigned == 0:
+                    return 0
+                return float(tickets.solved) / tickets.assigned
+        return 0
+
+    @classmethod
+    def getMaxAndAverageCodeContribution(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getCodeContribution() for x in lst])
+        averagecontribution=sum([x.getCodeContribution() for x in lst]) / n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageDiscussionContribution(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getDiscussionContribution() for x in lst])
+        averagecontribution=sum([x.getDiscussionContribution() for x in lst])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageTicketsSolvingPercentage(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getTicketsContribution() for x in lst])
+        averagecontribution=sum([x.getTicketsContribution() for x in lst])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    def codeRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        codcontr = self.getCodeContribution()
+        upper = len([x for x in lst if x.getCodeContribution() > codcontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def discussionRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        disccontr = self.getDiscussionContribution()
+        upper=len([x for x in lst if x.getDiscussionContribution()>disccontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def ticketsRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        ticketscontr = self.getTicketsContribution()
+        upper=len([x for x in lst if x.getTicketsContribution()>ticketscontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def getCommits(self, category = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None: 
+            return dict(number=0, lines=0)
+        cat = self.general[i]
+        j = getElementIndex(cat.commits, language = None)
+        if j is None:
+            return dict(number=0, lines=0)
+        return dict(
+            number=cat.commits[j]['number'], 
+            lines=cat.commits[j]['lines'])
+
+    def getArtifacts(self, category = None, art_type = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None:
+            return dict(created=0, modified=0)
+        cat = self.general[i]
+        j = getElementIndex(cat.messages, art_type = art_type)
+        if j is None:
+            return dict(created=0, modified=0)
+        return dict(created=cat[j].created, modified=cat[j].modified)
+
+    def getTickets(self, category = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None:
+            return dict(
+                assigned=0,
+                solved=0,
+                revoked=0,
+                averagesolvingtime=None)
+        if self.general[i].tickets.solved > 0:
+            tot = self.general[i].tickets.totsolvingtime 
+            number = self.general[i].tickets.solved
+            average = tot / number
+        else: 
+            average = None
+        return dict(
+            assigned=self.general[i].tickets.assigned,
+            solved=self.general[i].tickets.solved,
+            revoked=self.general[i].tickets.revoked,
+            averagesolvingtime=_convertTimeDiff(average))
+
+    def getCommitsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            i = getElementIndex(entry.commits, language = None)
+            if i is None: 
+                n, lines = 0, 0
+            else: 
+                n, lines = entry.commits[i].number, entry.commits[i].lines
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(number=n, lines=lines)
+        return by_cat
+
+    def getCommitsByLanguage(self):
+        langlist = []
+        by_lang = {}
+        i = getElementIndex(self.general, category=None)
+        if i is None: 
+            return dict(number=0, lines=0)
+        return dict([(el.language, dict(lines=el.lines, number=el.number))
+                     for el in self.general[i].commits])
+
+    def getArtifactsByCategory(self, detailed=False):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            if cat != None: 
+                cat = TroveCategory.query.get(_id = cat)
+            if detailed: 
+                by_cat[cat] = entry.messages
+            else:
+                i = getElementIndex(entry.messages, messagetype=None)
+                if i is not None:
+                    by_cat[cat] = entry.messages[i]
+                else: 
+                    by_cat[cat] = dict(created=0, modified=0)
+        return by_cat
+
+    def getArtifactsByType(self, category=None):
+        i = getElementIndex(self.general, category = category)
+        if i is None: 
+            return {}
+        entry = self.general[i].messages
+        by_type = dict([(el.messagetype, dict(created=el.created,
+                                              modified=el.modified))
+                         for el in entry])
+        return by_type
+
+    def getTicketsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            a, s = entry.tickets.assigned, entry.tickets.solved
+            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
+            if s:
+                average = time / s
+            else:
+                average = None
+            by_cat[cat] = dict(
+                assigned=a,
+                solved=s,
+                revoked=r, 
+                averagesolvingtime=_convertTimeDiff(average))
+        return by_cat
+
+    def getLastMonthCommits(self, category = None):
+        self.checkOldArtifacts() 
+        lineslist = [el.lines for el in self.lastmonth.commits
+                     if category in el.categories + [None]]
+        return dict(number=len(lineslist), lines=sum(lineslist))
+
+    def getLastMonthCommitsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist:
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if cat in el.categories + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(number=n, lines=lines)
+        return by_cat
+
+    def getLastMonthCommitsByLanguage(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        langlist=[el.language for el in self.general
+                  if el.language not in seen and not seen.add(el.language)]
+
+        by_lang = {}
+        for lang in langlist:
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if lang in el.programming_languages + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if lang != None:
+                lang = TroveCategory.query.get(_id = lang)
+            by_lang[lang] = dict(number=n, lines=lines)
+        return by_lang
+
+    def getLastMonthArtifacts(self, category = None):
+        self.checkOldArtifacts() 
+        cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
+                                     for el in self.lastmonth.messages
+                                     if category is None or 
+                                        category in el.categories], (0,0))
+        return dict(created=cre, modified=mod)
+
+    def getLastMonthArtifactsByType(self, category = None):
+        self.checkOldArtifacts()
+        seen = set()
+        types=[el.messagetype for el in self.lastmonth.messages
+               if el.messagetype not in seen and not seen.add(el.messagetype)]
+
+        by_type = {}
+        for t in types:
+            cre, mod = reduce(
+                addtuple, 
+                [(int(el.created),1-int(el.created))
+                 for el in self.lastmonth.messages
+                 if el.messagetype == t and
+                 category in [None]+el.categories],
+                (0,0))
+            by_type[t] = dict(created=cre, modified=mod)
+        return by_type
+
+    def getLastMonthArtifactsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist:
+            cre, mod = reduce(
+                addtuple, 
+                [(int(el.created),1-int(el.created))
+                 for el in self.lastmonth.messages 
+                 if cat in el.categories + [None]], (0,0))
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(created=cre, modified=mod)
+        return by_cat
+
+    def getLastMonthTickets(self, category = None):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts()
+        a = len([el for el in self.lastmonth.assignedtickets
+                 if category in el.categories + [None]])
+        r = len([el for el in self.lastmonth.revokedtickets
+                 if category in el.categories + [None]])
+        s, time = reduce(
+            addtuple, 
+            [(1, el.solvingtime)
+             for el in self.lastmonth.solvedtickets
+             if category in el.categories + [None]],
+            (0,0))
+        if category!=None:
+            category = TroveCategory.query.get(_id=category)
+        if s > 0:
+            time = time / s
+        else:
+            time = None
+        return dict(
+            assigned=a,
+            revoked=r,
+            solved=s, 
+            averagesolvingtime=_convertTimeDiff(time))
+        
+    def getLastMonthTicketsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts()
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+        by_cat = {}
+        for cat in catlist:
+            a = len([el for el in self.lastmonth.assignedtickets
+                     if cat in el.categories + [None]])
+            r = len([el for el in self.lastmonth.revokedtickets
+                     if cat in el.categories + [None]])
+            s, time = reduce(addtuple, [(1, el.solvingtime)
+                                        for el in self.lastmonth.solvedtickets
+                                        if cat in el.categories+[None]],(0,0))
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            if s > 0: 
+                time = time / s
+            else:
+                time = None
+            by_cat[cat] = dict(
+                assigned=a,
+                revoked=r,
+                solved=s, 
+                averagesolvingtime=_convertTimeDiff(time))
+        return by_cat
+        
+    def getLastMonthLogins(self):
+        self.checkOldArtifacts()
+        return len(self.lastmonth.logins)
+
+    def checkOldArtifacts(self):
+        now = datetime.now()
+        for m in self.lastmonth.messages:
+            if now - m.datetime > timedelta(30):
+                self.lastmonth.messages.remove(m)
+        for t in self.lastmonth.assignedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.assignedtickets.remove(t)
+        for t in self.lastmonth.revokedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.revokedtickets.remove(t)
+        for t in self.lastmonth.solvedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.solvedtickets.remove(t)
+
+    def addNewArtifact(self, art_type, art_datetime, project):
+        self._updateArtifactsStats(art_type, art_datetime, project, "created")
+
+    def addModifiedArtifact(self, art_type, art_datetime, project):
+        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
+
+    def addAssignedTicket(self, ticket, project):
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'assigned')
+        self.lastmonth.assignedtickets.append(
+            dict(datetime=ticket.mod_date, categories=topics))
+
+    def addRevokedTicket(self, ticket, project):
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'revoked')
+        self.lastmonth.revokedtickets.append(
+            dict(datetime=ticket.mod_date, categories=topics))
+        self.checkOldArtifacts()
+
+    def addClosedTicket(self, ticket, project):
+        topics = [t for t in project.trove_topic if t]
+        s_time=int((datetime.utcnow()-ticket.created_date).total_seconds())
+        self._updateTicketsStats(topics, 'solved', s_time = s_time)
+        self.lastmonth.solvedtickets.append(dict(
+            datetime=ticket.mod_date,
+            categories=topics,
+            solvingtime=s_time))
+        self.checkOldArtifacts()
+
+    def addCommit(self, newcommit, project):
+        def _addCommitData(stats, topics, languages, newblob, oldblob = None):
+            if oldblob:
+                listold = list(oldblob)
+            else:
+                listold = []
+            listnew = list(newblob)
+
+            if oldblob is None:
+                lines = len(listnew)
+            elif newblob.has_html_view:
+                diff = difflib.unified_diff(
+                    listold, listnew,
+                    ('old' + oldblob.path()).encode('utf-8'),
+                    ('new' + newblob.path()).encode('utf-8'))
+                lines = len([l for l in diff if len(l) > 0 and l[0] == '+'])-1
+            else:
+                lines = 0
+            
+            lt = topics + [None]
+            ll = languages + [None]
+            for t in lt:
+                i = getElementIndex(stats.general, category=t) 
+                if i is None:
+                    newstats = dict(
+                        category=t,
+                        commits=[],
+                        messages=dict(
+                            assigned=0,
+                            solved=0,
+                            revoked=0,
+                            totsolvingtime=0),
+                        tickets=[])   
+                    stats.general.append(newstats)
+                    i = getElementIndex(stats.general, category=t)
+                for lang in ll:
+                    j = getElementIndex(
+                        stats.general[i]['commits'], language=lang)
+                    if j is None:
+                        stats.general[i]['commits'].append(dict(
+                            language=lang, lines=lines, number=1))
+                    else:
+                        stats.general[i]['commits'][j].lines += lines
+                        stats.general[i]['commits'][j].number += 1
+            return lines
+
+        topics = [t for t in project.trove_topic if t]
+        languages = [l for l in project.trove_language if l]
+        now = datetime.utcnow()
+
+        d = newcommit.diffs
+        if len(newcommit.parent_ids) > 0:
+            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
+
+        totlines = 0
+        for changed in d.changed:
+            newblob = newcommit.tree.get_blob_by_path(changed)
+            oldblob = oldcommit.tree.get_blob_by_path(changed)
+            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+
+        for copied in d.copied:
+            newblob = newcommit.tree.get_blob_by_path(copied['new'])
+            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
+            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+
+        for added in d.added:
+            newblob = newcommit.tree.get_blob_by_path(added)
+            totlines+=_addCommitData(self, topics, languages, newblob)
+
+        self.lastmonth.commits.append(dict(
+            datetime=now, 
+            categories=topics, 
+            programming_languages=languages,
+            lines=totlines))
+        self.checkOldArtifacts()
+
+    def addLogin(self):
+        now = datetime.utcnow()
+        self.last_login = now
+        self.tot_logins_count += 1
+        self.lastmonth.logins.append(now)
+        self.checkOldArtifacts()
+        
+    def _updateArtifactsStats(self, art_type, art_datetime, project, action):
+        if action not in ['created', 'modified']: 
+            return
+        topics = [t for t in project.trove_topic if t]
+        lt = [None] + topics
+        for mtype in [None, art_type]:
+            for t in lt:
+                i = getElementIndex(self.general, category = t)
+                if i is None:
+                    msg = dict(
+                        category=t,
+                        commits=[],
+                        tickets=dict(
+                            solved=0,
+                            assigned=0,
+                            revoked=0,
+                            totsolvingtime=0),
+                        messages=[])
+                    self.general.append(msg)
+                    i = getElementIndex(self.general, category = t)
+                j = getElementIndex(
+                    self.general[i]['messages'], messagetype=mtype)
+                if j is None:
+                    entry = dict(messagetype=mtype, created=0, modified=0)
+                    entry[action] += 1
+                    self.general[i]['messages'].append(entry)
+                else:
+                    self.general[i]['messages'][j][action] += 1
+
+        self.lastmonth.messages.append(dict(
+            datetime=art_datetime,
+            created=(action == 'created'),
+            categories=topics,
+            messagetype=art_type))
+        self.checkOldArtifacts() 
+
+    def _updateTicketsStats(self, topics, action, s_time = None):
+        if action not in ['solved', 'assigned', 'revoked']:
+            return
+        lt = topics + [None]
+        for t in lt:
+            i = getElementIndex(self.general, category = t)
+            if i is None:
+                stats = dict(
+                    category=t,
+                    commits=[],
+                    tickets=dict(
+                        solved=0,
+                        assigned=0,
+                        revoked=0,
+                        totsolvingtime=0),
+                    messages=[])
+                self.general.append(stats)
+                i = getElementIndex(self.general, category = t)
+            self.general[i]['tickets'][action] += 1 
+            if action == 'solved': 
+                self.general[i]['tickets']['totsolvingtime']+=s_time
+
+def getElementIndex(el_list, **kw):
+    for i in range(len(el_list)):
+        for k in kw:
+            if el_list[i].get(k) != kw[k]:
+                break
+        else:
+            return i
+    return None
+
+def addtuple(l1, l2):
+    a, b = l1
+    x, y = l2
+    return (a+x, b+y)
+
+def _convertTimeDiff(int_seconds):
+    if int_seconds is None:
+        return None
+    diff = timedelta(seconds = int_seconds)
+    days, seconds = diff.days, diff.seconds
+    hours = seconds / 3600
+    seconds = seconds % 3600
+    minutes = seconds / 60
+    seconds = seconds % 60
+    return dict(
+        days=days, 
+        hours=hours, 
+        minutes=minutes,
+        seconds=seconds)
+
+Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops b/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
new file mode 100644
index 0000000..efae2aa
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/all-wcprops
@@ -0,0 +1,29 @@
+K 25
+svn:wc:ra_dav:version-url
+V 62
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates
+END
+commits.html
+K 25
+svn:wc:ra_dav:version-url
+V 75
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/commits.html
+END
+artifacts.html
+K 25
+svn:wc:ra_dav:version-url
+V 77
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/artifacts.html
+END
+tickets.html
+K 25
+svn:wc:ra_dav:version-url
+V 75
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/tickets.html
+END
+index.html
+K 25
+svn:wc:ra_dav:version-url
+V 73
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/templates/index.html
+END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/25f7657c/ForgeUserStats/forgeuserstats/templates/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/.svn/entries b/ForgeUserStats/forgeuserstats/templates/.svn/entries
new file mode 100644
index 0000000..ef7dfdb
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/templates/.svn/entries
@@ -0,0 +1,164 @@
+10
+
+dir
+4
+https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats/templates
+https://xp-dev.com/svn/allura
+
+
+
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+46ed536d-f66c-413e-a53e-834384f708db
+
+tickets.html
+file
+
+
+
+
+2012-11-05T14:43:25.725756Z
+4bac229c573965dbfd312e65cc7313a2
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1361
+
+index.html
+file
+
+
+
+
+2012-11-05T14:43:25.725756Z
+036136344f0b3099f212c6c749431996
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11126
+
+commits.html
+file
+
+
+
+
+2012-11-05T14:43:25.725756Z
+cbfcdaeb670c8896e31071077c51eb23
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+955
+
+artifacts.html
+file
+
+
+
+
+2012-11-05T14:43:25.725756Z
+bb6c7ceabf56de25d177ee5cd52451ab
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1386
+


[22/48] git commit: [5453] Correct .ini file for tests in ForgeUserStats

Posted by tv...@apache.org.
[5453] Correct .ini file for tests in ForgeUserStats


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

Branch: refs/heads/si/5453
Commit: 6abbf9dd7e9bcb52d363b78ad884e09348038b6b
Parents: 453f979
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Feb 1 21:25:01 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/test.ini |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6abbf9dd/ForgeUserStats/test.ini
----------------------------------------------------------------------
diff --git a/ForgeUserStats/test.ini b/ForgeUserStats/test.ini
index ca15ecd..04c1c6e 100644
--- a/ForgeUserStats/test.ini
+++ b/ForgeUserStats/test.ini
@@ -5,7 +5,7 @@
 #
 [DEFAULT]
 debug = true
-user.stats.enable = true
+userstats.enable = true
 
 [server:main]
 use = egg:Paste#http


[11/48] git commit: [#5778] Fixes tabindex values

Posted by tv...@apache.org.
[#5778] Fixes tabindex values


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

Branch: refs/heads/si/5453
Commit: b849642f6e45a0461dec602ea63ef7496c505720
Parents: d882b0b
Author: Chris Tsai <ct...@users.sf.net>
Authored: Tue Mar 19 17:04:53 2013 -0700
Committer: Chris Tsai <ct...@users.sf.net>
Committed: Tue Mar 19 17:06:10 2013 -0700

----------------------------------------------------------------------
 Allura/allura/public/nf/js/allura-base.js |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b849642f/Allura/allura/public/nf/js/allura-base.js
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/js/allura-base.js b/Allura/allura/public/nf/js/allura-base.js
index d1ae083..112cfe2 100644
--- a/Allura/allura/public/nf/js/allura-base.js
+++ b/Allura/allura/public/nf/js/allura-base.js
@@ -175,7 +175,7 @@ $(function(){
     // coming "in between" fields.
     var i = 0;
     $('input,textarea,select,button').each(function(){
-        $(this).attr('tabindex', i++);
+        $(this).attr('tabindex', ++i);
     });
 
     // Provide prompt text for otherwise empty viewers


[14/48] git commit: [5453] Fixed bug in user stats

Posted by tv...@apache.org.
[5453] Fixed bug in user stats


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

Branch: refs/heads/si/5453
Commit: 53e4e11f8cbad6b1e349cf6cb9a5bcb7e1b06321
Parents: 6c9b889
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 17:19:53 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/53e4e11f/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index d196709..0dcf5af 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -182,7 +182,7 @@ class UserStats(MappedClass):
         if i is None:
             return dict(created=0, modified=0)
         cat = self.general[i]
-        j = getElementIndex(cat.messages, art_type = art_type)
+        j = getElementIndex(cat.messages, messagetype = art_type)
         if j is None:
             return dict(created=0, modified=0)
         return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)


[31/48] git commit: [5453] Fixed stats template and improved controller

Posted by tv...@apache.org.
[5453] Fixed stats template and improved controller


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

Branch: refs/heads/si/5453
Commit: 7fdca7a19d98bb22f89174d9bfaa06654c019f0c
Parents: 4c2fdbe
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 16:21:49 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 .../forgeuserstats/controllers/userstats.py        |   33 +++++++--------
 ForgeUserStats/forgeuserstats/templates/index.html |   12 +++---
 2 files changed, 21 insertions(+), 24 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7fdca7a1/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index fe14449..b84eb0f 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -15,16 +15,15 @@ class ForgeUserStatsController(BaseController):
         if not self.user:
             return ForgeUserStatsController(user=user), remainder
         if part == "category":
-            return ForgeUserStatsCatController(self.user, self.stats, None), remainder
+            return ForgeUserStatsCatController(self.user, None), remainder
         if part == "metric":
-            return ForgeUserStatsMetricController(self.user, self.stats), remainder
+            return ForgeUserStatsMetricController(self.user), remainder
 
     def __init__(self, user=None):
         self.user = user
         if self.user:
-            self.stats = self.user.stats
-            if not self.stats:
-                self.stats = UserStats.create(self.user)
+            if not user.stats:
+                UserStats.create(self.user)
 
         super(ForgeUserStatsController, self).__init__()
 
@@ -33,7 +32,7 @@ class ForgeUserStatsController(BaseController):
     def index(self, **kw):
         if not self.user: 
             return dict(user=None)
-        stats = self.stats
+        stats = self.user.stats
 
         ret_dict = _getDataForCategory(None, stats)
         ret_dict['user'] = self.user
@@ -107,25 +106,24 @@ class ForgeUserStatsController(BaseController):
 
     @expose()
     def code_ranking_bar(self):
-        return create_progress_bar(self.stats.codeRanking())
+        return create_progress_bar(self.user.stats.codeRanking())
 
     @expose()
     def discussion_ranking_bar(self):
-        return create_progress_bar(self.stats.discussionRanking())
+        return create_progress_bar(self.user.stats.discussionRanking())
 
     @expose()
     def tickets_ranking_bar(self):
-        return create_progress_bar(self.stats.ticketsRanking())
+        return create_progress_bar(self.user.stats.ticketsRanking())
 
 class ForgeUserStatsCatController(BaseController):
     @expose()
     def _lookup(self, category, *remainder):
-        cat = M.TroveCategory.query.get(fullname=category)
+        cat = M.TroveCategory.query.get(shortname=category)
         return ForgeUserStatsCatController(self.user, cat), remainder
 
-    def __init__(self, user, stats, category):
+    def __init__(self, user, category):
         self.user = user
-        self.stats = stats
         self.category = category
         super(ForgeUserStatsCatController, self).__init__()
 
@@ -134,7 +132,7 @@ class ForgeUserStatsCatController(BaseController):
     def index(self, **kw):
         if not self.user:
             return dict(user=None)
-        stats = self.stats
+        stats = self.user.stats
         
         cat_id = None
         if self.category: 
@@ -148,9 +146,8 @@ class ForgeUserStatsCatController(BaseController):
 
 class ForgeUserStatsMetricController(BaseController):
 
-    def __init__(self, user, stats):
+    def __init__(self, user):
         self.user = user
-        self.stats = stats
         super(ForgeUserStatsMetricController, self).__init__()
 
     @expose('jinja:forgeuserstats:templates/commits.html')
@@ -158,7 +155,7 @@ class ForgeUserStatsMetricController(BaseController):
     def commits(self, **kw):
         if not self.user:
             return dict(user=None)
-        stats = self.stats
+        stats = self.user.stats
         
         commits = stats.getCommitsByCategory()
         return dict(user = self.user,
@@ -170,7 +167,7 @@ class ForgeUserStatsMetricController(BaseController):
         if not self.user:
             return dict(user=None)
 
-        stats = self.stats       
+        stats = self.user.stats       
         artifacts = stats.getArtifactsByCategory(detailed=True)
         return dict(user = self.user, data = artifacts) 
 
@@ -180,7 +177,7 @@ class ForgeUserStatsMetricController(BaseController):
         if not self.user: 
             return dict(user=None)
 
-        artifacts = self.stats.getTicketsByCategory()
+        artifacts = self.user.stats.getTicketsByCategory()
         return dict(user = self.user, data = artifacts) 
 
 def _getDataForCategory(category, stats):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7fdca7a1/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index f5a71c6..6d6a25b 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -85,9 +85,9 @@
           <td>{{lastmonthcommits.number}}</td>
           {% if days >= 30 %}
             <td style="text-align:center;">
-              {% if permonthcommits.number > permonthcommits.number %}
+              {% if lastmonthcommits.number > permonthcommits.number %}
                   <img src="{{g.forge_static('images/up.png')}}"/>
-              {% elif permonthcommits.number == permonthcommits.number %}
+              {% elif lastmonthcommits.number == permonthcommits.number %}
                 <img src="{{g.forge_static('images/equal.png')}}"/>
               {% else %}
                 <img src="{{g.forge_static('images/down.png')}}"/>
@@ -108,9 +108,9 @@
           <td>{{lastmonthcommits.lines}}</td>
           {% if days >= 30 %}
             <td style="text-align:center;">
-              {% if permonthcommits.lines > permonthcommits.lines %}
+              {% if lastmonthcommits.lines > permonthcommits.lines %}
                   <img src="{{g.forge_static('images/up.png')}}"/>
-              {% elif permonthcommits.lines == permonthcommits.lines %}
+              {% elif lastmonthcommits.lines == permonthcommits.lines %}
                 <img src="{{g.forge_static('images/equal.png')}}"/>
               {% else %}
                 <img src="{{g.forge_static('images/down.png')}}"/>
@@ -193,7 +193,7 @@
                   {% else %}
                     <img src="{{g.forge_static('images/down.png')}}"/>
                   {%endif%}
-                {%else%} Down {%endif%}
+                {%else%} <img src="{{g.forge_static('images/down.png')}}"/> {%endif%}
               </td>
             {% endif %}
           </tr>
@@ -224,7 +224,7 @@
                   {% else %}
                     <img src="{{g.forge_static('images/down.png')}}"/>
                   {%endif%}
-                {%else%} Down {%endif%}
+                {%else%} <img src="{{g.forge_static('images/down.png')}}"/> {%endif%}
               </td>
             {% endif %}
           </tr>


[36/48] git commit: Fix pylons globals imports

Posted by tv...@apache.org.
Fix pylons globals imports


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

Branch: refs/heads/si/5453
Commit: e37eec846077364b64123dde2a641f48de09bcf9
Parents: e72efa8
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Mon Feb 25 21:55:33 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:55 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py               |    3 ++-
 .../forgeuserstats/controllers/userstats.py        |    2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/e37eec84/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index d9cada2..4d9e1e9 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -1,5 +1,6 @@
 import pymongo
-from pylons import c, g, request
+from pylons import tmpl_context as c, app_globals as g
+from pylons import request
 
 import bson
 from ming import schema as S

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/e37eec84/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index c77dded..40ad92c 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -5,7 +5,7 @@ from allura.controllers import BaseController
 import allura.model as M
 from allura.lib.graphics.graphic_methods import create_histogram, create_progress_bar
 from forgeuserstats.model.stats import UserStats
-from pylons import c
+from pylons import tmpl_context as c
 
 class ForgeUserStatsController(BaseController):
 


[34/48] git commit: [5453] removed duplicated links

Posted by tv...@apache.org.
[5453] removed duplicated links


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

Branch: refs/heads/si/5453
Commit: 28d50bb1c1b1bb207844eeaa7ce5a03c1d5c9090
Parents: 0b5e176
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:44:14 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 .../forgeuserstats/templates/commits.html          |    1 -
 .../forgeuserstats/templates/tickets.html          |    1 -
 2 files changed, 0 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/28d50bb1/ForgeUserStats/forgeuserstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/commits.html b/ForgeUserStats/forgeuserstats/templates/commits.html
index 2bca003..12f9712 100644
--- a/ForgeUserStats/forgeuserstats/templates/commits.html
+++ b/ForgeUserStats/forgeuserstats/templates/commits.html
@@ -37,6 +37,5 @@
       </table>
     </div>
     {% endif %}
-    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
   {% endif %}
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/28d50bb1/ForgeUserStats/forgeuserstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/tickets.html b/ForgeUserStats/forgeuserstats/templates/tickets.html
index 9bf411b..4604c16 100644
--- a/ForgeUserStats/forgeuserstats/templates/tickets.html
+++ b/ForgeUserStats/forgeuserstats/templates/tickets.html
@@ -47,6 +47,5 @@
       </table>
     </div>
     {% endif %}
-    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
   {% endif %}
 {% endblock %}


[12/48] git commit: [5453] Added some tests

Posted by tv...@apache.org.
[5453] Added some tests


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

Branch: refs/heads/si/5453
Commit: 1128280c7f8b446f86865e8d419d47056aefb3aa
Parents: 1723f17
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 19:08:03 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/tests/test_stats.py |  173 ++++++++++++++++
 1 files changed, 173 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1128280c/ForgeUserStats/forgeuserstats/tests/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/__init__.py b/ForgeUserStats/forgeuserstats/tests/__init__.py
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1128280c/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
new file mode 100644
index 0000000..f0ee6a2
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -0,0 +1,173 @@
+import pkg_resources
+
+from pylons import app_globals as g
+from pylons import tmpl_context as c
+
+from alluratest.controller import TestController
+from allura.tests import decorators as td
+from allura.lib import helpers as h
+from allura.model import User
+
+from forgewiki import model as WM
+from forgetracker import model as TM
+
+class TestStats(TestController):
+
+    test_username = 'teststats'
+    test_password = 'foo'
+
+    def setUp(self):
+        super(TestStats, self).setUp()
+        for ep in pkg_resources.iter_entry_points("allura.stats"):
+            if ep.name.lower() == 'userstats':
+                g.statslisteners = [ep.load()().listener]
+
+        self.user = User.register(dict(username=self.test_username,
+            display_name='Test Stats'),
+            make_project=False)
+        self.user.set_password(self.test_password)
+        
+    def test_init_values(self):
+        artifacts = self.user.stats.getArtifacts()
+        tickets = self.user.stats.getTickets()
+        commits = self.user.stats.getCommits()
+        assert self.user.stats.tot_logins_count == 0
+        assert artifacts['created'] == 0
+        assert artifacts['modified'] == 0
+        assert tickets['assigned'] == 0
+        assert tickets['solved'] == 0
+        assert tickets['revoked'] == 0
+        assert tickets['averagesolvingtime'] is None
+        assert commits['number'] == 0
+        assert commits['lines'] == 0
+
+        lmartifacts = self.user.stats.getLastMonthArtifacts()
+        lmtickets = self.user.stats.getLastMonthTickets()
+        lmcommits = self.user.stats.getLastMonthCommits()
+        assert self.user.stats.getLastMonthLogins() == 0
+        assert lmartifacts['created'] == 0
+        assert lmartifacts['modified'] == 0
+        assert lmtickets['assigned'] == 0
+        assert lmtickets['solved'] == 0
+        assert lmtickets['revoked'] == 0
+        assert lmtickets['averagesolvingtime'] is None
+        assert lmcommits['number'] == 0
+        assert lmcommits['lines'] == 0
+
+    def test_login(self):
+        init_logins = self.user.stats.tot_logins_count
+        r = self.app.post('/auth/do_login', params=dict(
+                username=self.test_username, password=self.test_password))
+
+        assert self.user.stats.tot_logins_count == 1 + init_logins
+        assert self.user.stats.getLastMonthLogins() == 1 + init_logins
+
+    @td.with_user_project('test-admin')
+    @td.with_wiki
+    def test_wiki_stats(self):
+        initial_artifacts = c.user.stats.getArtifacts()
+        initial_wiki = c.user.stats.getArtifacts(art_type="Wiki")
+
+        h.set_context('test', 'wiki', neighborhood='Projects')
+        page = WM.Page(title="TestPage", text="some text")
+        page.commit()
+
+        artifacts = c.user.stats.getArtifacts()
+        wiki = c.user.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 1 + initial_artifacts['created']
+        assert artifacts['modified'] == initial_artifacts['modified']
+        assert wiki['created'] == 1 + initial_wiki['created']
+        assert wiki['modified'] == initial_wiki['modified']
+
+        page = WM.Page(title="TestPage2", text="some different text")
+        page.commit()
+
+        artifacts = c.user.stats.getArtifacts()
+        wiki = c.user.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == initial_wiki['modified']
+
+
+        page.text="some modified text"
+        page.commit()
+
+        artifacts = c.user.stats.getArtifacts()
+        wiki = c.user.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == 1 + initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == 1 + initial_wiki['modified']
+
+
+    @td.with_user_project('test-admin')
+    @td.with_tracker
+    def test_tracker_stats(self):
+        initial_tickets = c.user.stats.getTickets()
+        initial_tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+
+        h.set_context('test', 'bugs', neighborhood='Projects')
+        ticket = TM.Ticket(ticket_num=12, summary="test", assigned_to_id = c.user._id)
+        ticket.commit()
+
+        tickets = c.user.stats.getTickets()
+        tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 1
+        assert tickets['solved'] == initial_tickets['solved']
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 1
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified']
+
+        ticket.status = 'closed'
+        ticket.commit()
+
+        tickets = c.user.stats.getTickets()
+        tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 1
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 1
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 1
+
+        h.set_context('test', 'bugs', neighborhood='Projects')
+        ticket = TM.Ticket(ticket_num=13, summary="test")
+        ticket.commit()
+        
+        tickets = c.user.stats.getTickets()
+        tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 1
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 1
+
+        ticket.assigned_to_id = c.user._id
+        ticket.commit()
+
+        tickets = c.user.stats.getTickets()
+        tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+        
+        assert tickets['assigned'] == initial_tickets['assigned'] + 2
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 2
+
+        ticket.assigned_to_id = self.user._id
+        ticket.commit()
+
+        tickets = c.user.stats.getTickets()
+        tickets_artifacts = c.user.stats.getArtifacts(art_type="Ticket")
+        
+        assert tickets['assigned'] == initial_tickets['assigned'] + 2
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked'] + 1
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 3


[07/48] git commit: [#5909] Fixed user_skills redirect on add / remove

Posted by tv...@apache.org.
[#5909] Fixed user_skills redirect on add / remove

Signed-off-by: Cory Johns <jo...@geek.net>


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

Branch: refs/heads/si/5453
Commit: 9d3caf32deb40e2c7107d520703198e714e9ba4a
Parents: 7eee5d9
Author: Cory Johns <jo...@geek.net>
Authored: Wed Mar 13 19:45:41 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:47 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py |    4 ++--
 1 files changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/9d3caf32/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 7abdbc8..2914799 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -508,7 +508,7 @@ class UserSkillsController(BaseController):
         s.append(new_skill)
         c.user.set_pref('skills', s)
         flash('Your skills list was successfully updated!')
-        redirect('..')
+        redirect('.')
 
     @expose()
     @require_post()
@@ -521,7 +521,7 @@ class UserSkillsController(BaseController):
              if str(skill.category_id) != str(category._id)]
         c.user.set_pref('skills', s)
         flash('Your skills list was successfully updated!')
-        redirect('..')
+        redirect('.')
 
 class UserContactsController(BaseController):
 


[48/48] git commit: [#5453] Correctly initialize user stats

Posted by tv...@apache.org.
[#5453] Correctly initialize user stats


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

Branch: refs/heads/si/5453
Commit: ea7b7087a4692fb5ef905dbe44c2f31a6798bb51
Parents: 5d10f44
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Mar 7 23:44:50 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py |    6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/ea7b7087/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index 51121ad..5a4751a 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -498,12 +498,12 @@ class Stats(MappedClass):
                     newstats = dict(
                         category=t,
                         commits=[],
-                        messages=[dict(
+                        messages=[],
+                        tickets=dict(
                             assigned=0,
                             solved=0,
                             revoked=0,
-                            totsolvingtime=0)],
-                        tickets={})
+                            totsolvingtime=0))
                     stats.general.append(newstats)
                     i = getElementIndex(stats.general, category=t)
                 for lang in ll:


[15/48] git commit: [5453] Adding tests and fixing errors related to commit stats

Posted by tv...@apache.org.
[5453] Adding tests and fixing errors related to commit stats


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

Branch: refs/heads/si/5453
Commit: 1528fc9589cd71db6ee36214c39f5bd2875ad00c
Parents: 1128280
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 12 18:37:26 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py       |   22 ++-
 .../forgeuserstats/tests/data/testgit.git/HEAD     |    1 +
 .../forgeuserstats/tests/data/testgit.git/config   |    4 +
 .../tests/data/testgit.git/description             |    1 +
 .../data/testgit.git/hooks/applypatch-msg.sample   |   15 ++
 .../tests/data/testgit.git/hooks/commit-msg.sample |   24 ++
 .../data/testgit.git/hooks/post-commit.sample      |    8 +
 .../tests/data/testgit.git/hooks/post-receive      |    1 +
 .../data/testgit.git/hooks/post-receive.sample     |   15 ++
 .../data/testgit.git/hooks/post-update.sample      |    8 +
 .../data/testgit.git/hooks/pre-applypatch.sample   |   14 ++
 .../tests/data/testgit.git/hooks/pre-commit.sample |   46 ++++
 .../tests/data/testgit.git/hooks/pre-rebase.sample |  169 +++++++++++++++
 .../testgit.git/hooks/prepare-commit-msg.sample    |   36 +++
 .../tests/data/testgit.git/hooks/update            |    1 +
 .../tests/data/testgit.git/hooks/update.sample     |  128 +++++++++++
 .../tests/data/testgit.git/info/exclude            |    6 +
 .../0d/666fc313e6f4829e8d4446d4529394f8e464a9      |  Bin 0 -> 29 bytes
 .../0f/419b54b956f4a849a689105216282f1a4c749a      |  Bin 0 -> 51 bytes
 .../1e/146e67985dcd71c74de79613719bef7bddca4a      |  Bin 0 -> 164 bytes
 .../43/80f66cf5cbd858b73156681d665593ec80a58d      |  Bin 0 -> 43 bytes
 .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904      |  Bin 0 -> 15 bytes
 .../6a/45885ae7347f1cac5103b0050cc1be6a1496c8      |  Bin 0 -> 158 bytes
 .../6d/2ce67bdd55ae2c2ac72cbc879ed7c67ecc9786      |  Bin 0 -> 43 bytes
 .../8c/3c7fbcd903744b20fd7567a1fcefa99133b5bc      |  Bin 0 -> 53 bytes
 .../9a/7df788cf800241e3bb5a849c8870f2f8259d98      |  Bin 0 -> 129 bytes
 .../be/00c63250248c284b842deee5d8fb0b8132acab      |  Bin 0 -> 42 bytes
 .../d7/c40db3ffe2b87e96b94c280a67265c8de7a4ad      |  Bin 0 -> 51 bytes
 .../df/30427c488aeab84b2352bdf88a3b19223f9d7a      |    2 +
 .../e5/6a4d15295d3754310f114c86d93645308110ad      |  Bin 0 -> 43 bytes
 .../e9/65047ad7c57865823c7d992b1d046ea66edf78      |  Bin 0 -> 21 bytes
 .../tests/data/testgit.git/refs/heads/master       |    1 +
 ForgeUserStats/forgeuserstats/tests/test_stats.py  |   42 ++++-
 ForgeUserStats/test.ini                            |   54 +++++
 34 files changed, 589 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 0dcf5af..2f5e1b9 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -475,16 +475,19 @@ class UserStats(MappedClass):
         self.checkOldArtifacts()
 
     def addCommit(self, newcommit, project):
-        def _addCommitData(stats, topics, languages, newblob, oldblob = None):
+        def _computeLines(newblob, oldblob = None):
             if oldblob:
                 listold = list(oldblob)
             else:
                 listold = []
-            listnew = list(newblob)
+            if newblob:
+                listnew = list(newblob)
+            else:
+                listnew = []
 
             if oldblob is None:
                 lines = len(listnew)
-            elif newblob.has_html_view:
+            elif newblob and newblob.has_html_view:
                 diff = difflib.unified_diff(
                     listold, listnew,
                     ('old' + oldblob.path()).encode('utf-8'),
@@ -492,7 +495,9 @@ class UserStats(MappedClass):
                 lines = len([l for l in diff if len(l) > 0 and l[0] == '+'])-1
             else:
                 lines = 0
-            
+            return lines
+
+        def _addCommitData(stats, topics, languages, lines):          
             lt = topics + [None]
             ll = languages + [None]
             for t in lt:
@@ -518,7 +523,6 @@ class UserStats(MappedClass):
                     else:
                         stats.general[i]['commits'][j].lines += lines
                         stats.general[i]['commits'][j].number += 1
-            return lines
 
         topics = [t for t in project.trove_topic if t]
         languages = [l for l in project.trove_language if l]
@@ -532,16 +536,18 @@ class UserStats(MappedClass):
         for changed in d.changed:
             newblob = newcommit.tree.get_blob_by_path(changed)
             oldblob = oldcommit.tree.get_blob_by_path(changed)
-            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+            totlines+=_computeLines(newblob, oldblob)
 
         for copied in d.copied:
             newblob = newcommit.tree.get_blob_by_path(copied['new'])
             oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
-            totlines+=_addCommitData(self, topics, languages, newblob, oldblob)
+            totlines+=_computeLines(newblob, oldblob)
 
         for added in d.added:
             newblob = newcommit.tree.get_blob_by_path(added)
-            totlines+=_addCommitData(self, topics, languages, newblob)
+            totlines+=_computeLines(newblob)
+
+        _addCommitData(self, topics, languages, totlines)
 
         self.lastmonth.commits.append(dict(
             datetime=now, 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/HEAD
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/HEAD b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/HEAD
new file mode 100644
index 0000000..cb089cd
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/config
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/config b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/config
new file mode 100644
index 0000000..07d359d
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/config
@@ -0,0 +1,4 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = true

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/description
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/description b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/description
new file mode 100644
index 0000000..498b267
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/applypatch-msg.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/applypatch-msg.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/applypatch-msg.sample
new file mode 100755
index 0000000..8b2a2fe
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/applypatch-msg.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.  The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "applypatch-msg".
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/commit-msg" &&
+	exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/commit-msg.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/commit-msg.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/commit-msg.sample
new file mode 100755
index 0000000..6ef1d29
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/commit-msg.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by git-commit with one argument, the name of the file
+# that has the commit message.  The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit.  The hook is allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "commit-msg".
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
+# hook is more suited to it.
+#
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+	 sort | uniq -c | sed -e '/^[ 	]*1[ 	]/d')" || {
+	echo >&2 Duplicate Signed-off-by lines.
+	exit 1
+}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-commit.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-commit.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-commit.sample
new file mode 100755
index 0000000..2266821
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-commit.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script that is called after a successful
+# commit is made.
+#
+# To enable this hook, rename this file to "post-commit".
+
+: Nothing

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive
new file mode 100755
index 0000000..0f7a148
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive
@@ -0,0 +1 @@
+post-receive

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive.sample
new file mode 100755
index 0000000..7a83e17
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script for the "post-receive" event.
+#
+# The "post-receive" script is run after receive-pack has accepted a pack
+# and the repository has been updated.  It is passed arguments in through
+# stdin in the form
+#  <oldrev> <newrev> <refname>
+# For example:
+#  aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
+#
+# see contrib/hooks/ for a sample, or uncomment the next line and
+# rename the file to "post-receive".
+
+#. /usr/share/doc/git-core/contrib/hooks/post-receive-email

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-update.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-update.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-update.sample
new file mode 100755
index 0000000..5323b56
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-update.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, rename this file to "post-update".
+
+exec git-update-server-info

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-applypatch.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-applypatch.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-applypatch.sample
new file mode 100755
index 0000000..b1f187c
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-applypatch.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-applypatch".
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+	exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-commit.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-commit.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-commit.sample
new file mode 100755
index 0000000..439eefd
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-commit.sample
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by git-commit with no arguments.  The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git-rev-parse --verify HEAD >/dev/null 2>&1
+then
+	against=HEAD
+else
+	# Initial commit: diff against an empty tree object
+	against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+fi
+
+# If you want to allow non-ascii filenames set this variable to true.
+allownonascii=$(git config hooks.allownonascii)
+
+# Cross platform projects tend to avoid non-ascii filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+	# Note that the use of brackets around a tr range is ok here, (it's
+	# even required, for portability to Solaris 10's /usr/bin/tr), since
+	# the square bracket bytes happen to fall in the designated range.
+	test "$(git diff --cached --name-only --diff-filter=A -z $against |
+	  LC_ALL=C tr -d '[ -~]\0')"
+then
+	echo "Error: Attempt to add a non-ascii file name."
+	echo
+	echo "This can cause problems if you want to work"
+	echo "with people on other platforms."
+	echo
+	echo "To be portable it is advisable to rename the file ..."
+	echo
+	echo "If you know what you are doing you can disable this"
+	echo "check using:"
+	echo
+	echo "  git config hooks.allownonascii true"
+	echo
+	exit 1
+fi
+
+exec git diff-index --check --cached $against --

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-rebase.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-rebase.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-rebase.sample
new file mode 100755
index 0000000..be1b06e
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-rebase.sample
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, 2008 Junio C Hamano
+#
+# The "pre-rebase" hook is run just before "git-rebase" starts doing
+# its job, and can prevent the command from running by exiting with
+# non-zero status.
+#
+# The hook is called with the following parameters:
+#
+# $1 -- the upstream the series was forked from.
+# $2 -- the branch being rebased (or empty when rebasing the current branch).
+#
+# This sample shows how to prevent topic branches that are already
+# merged to 'next' branch from getting rebased, because allowing it
+# would result in rebasing already published history.
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+	topic="refs/heads/$2"
+else
+	topic=`git symbolic-ref HEAD` ||
+	exit 0 ;# we do not interrupt rebasing detached HEAD
+fi
+
+case "$topic" in
+refs/heads/??/*)
+	;;
+*)
+	exit 0 ;# we do not interrupt others.
+	;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master.  Is it OK to rebase it?
+
+# Does the topic really exist?
+git show-ref -q "$topic" || {
+	echo >&2 "No such branch $topic"
+	exit 1
+}
+
+# Is topic fully merged to master?
+not_in_master=`git-rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+	echo >&2 "$topic is fully merged to master; better remove it."
+	exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next?  If so you should not be rebasing it.
+only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git-rev-list ^master           ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+	not_in_topic=`git-rev-list "^$topic" master`
+	if test -z "$not_in_topic"
+	then
+		echo >&2 "$topic is already up-to-date with master"
+		exit 1 ;# we could allow it, but there is no point.
+	else
+		exit 0
+	fi
+else
+	not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"`
+	perl -e '
+		my $topic = $ARGV[0];
+		my $msg = "* $topic has commits already merged to public branch:\n";
+		my (%not_in_next) = map {
+			/^([0-9a-f]+) /;
+			($1 => 1);
+		} split(/\n/, $ARGV[1]);
+		for my $elem (map {
+				/^([0-9a-f]+) (.*)$/;
+				[$1 => $2];
+			} split(/\n/, $ARGV[2])) {
+			if (!exists $not_in_next{$elem->[0]}) {
+				if ($msg) {
+					print STDERR $msg;
+					undef $msg;
+				}
+				print STDERR " $elem->[1]\n";
+			}
+		}
+	' "$topic" "$not_in_next" "$not_in_master"
+	exit 1
+fi
+
+exit 0
+
+################################################################
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+   merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+   it is deleted.  If you need to build on top of it to correct
+   earlier mistakes, a new topic branch is created by forking at
+   the tip of the "master".  This is not strictly necessary, but
+   it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+   branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next".  Young
+    topic branches can have stupid mistakes you would rather
+    clean up before publishing, and things that have not been
+    merged into other branches can be easily rebased without
+    affecting other people.  But once it is published, you would
+    not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+    Then you can delete it.  More importantly, you should not
+    build on top of it -- other people may already want to
+    change things related to the topic as patches against your
+    "master", so if you need further changes, it is better to
+    fork the topic (perhaps with the same name) afresh from the
+    tip of "master".
+
+Let's look at this example:
+
+		   o---o---o---o---o---o---o---o---o---o "next"
+		  /       /           /           /
+		 /   a---a---b A     /           /
+		/   /               /           /
+	       /   /   c---c---c---c B         /
+	      /   /   /             \         /
+	     /   /   /   b---b C     \       /
+	    /   /   /   /             \     /
+    ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished.  It has been fully merged up to "master" and "next",
+   and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+	git-rev-list ^master ^topic next
+	git-rev-list ^master        next
+
+	if these match, topic has not merged in next at all.
+
+To compute (2):
+
+	git-rev-list master..topic
+
+	if this is empty, it is fully merged to "master".

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/prepare-commit-msg.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/prepare-commit-msg.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/prepare-commit-msg.sample
new file mode 100755
index 0000000..3652424
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/prepare-commit-msg.sample
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# An example hook script to prepare the commit log message.
+# Called by git-commit with the name of the file that has the
+# commit message, followed by the description of the commit
+# message's source.  The hook's purpose is to edit the commit
+# message file.  If the hook fails with a non-zero status,
+# the commit is aborted.
+#
+# To enable this hook, rename this file to "prepare-commit-msg".
+
+# This hook includes three examples.  The first comments out the
+# "Conflicts:" part of a merge commit.
+#
+# The second includes the output of "git diff --name-status -r"
+# into the message, just before the "git status" output.  It is
+# commented because it doesn't cope with --amend or with squashed
+# commits.
+#
+# The third example adds a Signed-off-by line to the message, that can
+# still be edited.  This is rarely a good idea.
+
+case "$2,$3" in
+  merge,)
+    perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
+
+# ,|template,)
+#   perl -i.bak -pe '
+#      print "\n" . `git diff --cached --name-status -r`
+#	 if /^#/ && $first++ == 0' "$1" ;;
+
+  *) ;;
+esac
+
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update
new file mode 100755
index 0000000..4ea5e4d
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update
@@ -0,0 +1 @@
+update

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update.sample
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update.sample b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update.sample
new file mode 100755
index 0000000..fd63b2d
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update.sample
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# An example hook script to blocks unannotated tags from entering.
+# Called by git-receive-pack with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, rename this file to "update".
+#
+# Config
+# ------
+# hooks.allowunannotated
+#   This boolean sets whether unannotated tags will be allowed into the
+#   repository.  By default they won't be.
+# hooks.allowdeletetag
+#   This boolean sets whether deleting tags will be allowed in the
+#   repository.  By default they won't be.
+# hooks.allowmodifytag
+#   This boolean sets whether a tag may be modified after creation. By default
+#   it won't be.
+# hooks.allowdeletebranch
+#   This boolean sets whether deleting branches will be allowed in the
+#   repository.  By default they won't be.
+# hooks.denycreatebranch
+#   This boolean sets whether remotely creating branches will be denied
+#   in the repository.  By default this is allowed.
+#
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+	echo "Don't run this script from the command line." >&2
+	echo " (if you want, you could supply GIT_DIR then run" >&2
+	echo "  $0 <ref> <oldrev> <newrev>)" >&2
+	exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+	echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
+	exit 1
+fi
+
+# --- Config
+allowunannotated=$(git config --bool hooks.allowunannotated)
+allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
+denycreatebranch=$(git config --bool hooks.denycreatebranch)
+allowdeletetag=$(git config --bool hooks.allowdeletetag)
+allowmodifytag=$(git config --bool hooks.allowmodifytag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+case "$projectdesc" in
+"Unnamed repository"* | "")
+	echo "*** Project description file hasn't been set" >&2
+	exit 1
+	;;
+esac
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+zero="0000000000000000000000000000000000000000"
+if [ "$newrev" = "$zero" ]; then
+	newrev_type=delete
+else
+	newrev_type=$(git-cat-file -t $newrev)
+fi
+
+case "$refname","$newrev_type" in
+	refs/tags/*,commit)
+		# un-annotated tag
+		short_refname=${refname##refs/tags/}
+		if [ "$allowunannotated" != "true" ]; then
+			echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
+			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+			exit 1
+		fi
+		;;
+	refs/tags/*,delete)
+		# delete tag
+		if [ "$allowdeletetag" != "true" ]; then
+			echo "*** Deleting a tag is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/tags/*,tag)
+		# annotated tag
+		if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
+		then
+			echo "*** Tag '$refname' already exists." >&2
+			echo "*** Modifying a tag is not allowed in this repository." >&2
+			exit 1
+		fi
+		;;
+	refs/heads/*,commit)
+		# branch
+		if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
+			echo "*** Creating a branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/heads/*,delete)
+		# delete branch
+		if [ "$allowdeletebranch" != "true" ]; then
+			echo "*** Deleting a branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	refs/remotes/*,commit)
+		# tracking branch
+		;;
+	refs/remotes/*,delete)
+		# delete tracking branch
+		if [ "$allowdeletebranch" != "true" ]; then
+			echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+			exit 1
+		fi
+		;;
+	*)
+		# Anything else (is there anything else?)
+		echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+		exit 1
+		;;
+esac
+
+# --- Finished
+exit 0

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/info/exclude
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/info/exclude b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/info/exclude
new file mode 100644
index 0000000..2c87b72
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/info/exclude
@@ -0,0 +1,6 @@
+# git-ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0d/666fc313e6f4829e8d4446d4529394f8e464a9
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0d/666fc313e6f4829e8d4446d4529394f8e464a9 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0d/666fc313e6f4829e8d4446d4529394f8e464a9
new file mode 100644
index 0000000..934760d
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0d/666fc313e6f4829e8d4446d4529394f8e464a9 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0f/419b54b956f4a849a689105216282f1a4c749a
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0f/419b54b956f4a849a689105216282f1a4c749a b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0f/419b54b956f4a849a689105216282f1a4c749a
new file mode 100644
index 0000000..a7d3884
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/0f/419b54b956f4a849a689105216282f1a4c749a differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/1e/146e67985dcd71c74de79613719bef7bddca4a
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/1e/146e67985dcd71c74de79613719bef7bddca4a b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/1e/146e67985dcd71c74de79613719bef7bddca4a
new file mode 100644
index 0000000..2f0027a
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/1e/146e67985dcd71c74de79613719bef7bddca4a differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/43/80f66cf5cbd858b73156681d665593ec80a58d
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/43/80f66cf5cbd858b73156681d665593ec80a58d b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/43/80f66cf5cbd858b73156681d665593ec80a58d
new file mode 100644
index 0000000..fe031e3
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/43/80f66cf5cbd858b73156681d665593ec80a58d differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
new file mode 100644
index 0000000..adf6411
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6a/45885ae7347f1cac5103b0050cc1be6a1496c8
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6a/45885ae7347f1cac5103b0050cc1be6a1496c8 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6a/45885ae7347f1cac5103b0050cc1be6a1496c8
new file mode 100644
index 0000000..42efcfb
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6a/45885ae7347f1cac5103b0050cc1be6a1496c8 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6d/2ce67bdd55ae2c2ac72cbc879ed7c67ecc9786
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6d/2ce67bdd55ae2c2ac72cbc879ed7c67ecc9786 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6d/2ce67bdd55ae2c2ac72cbc879ed7c67ecc9786
new file mode 100644
index 0000000..838b73a
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/6d/2ce67bdd55ae2c2ac72cbc879ed7c67ecc9786 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/8c/3c7fbcd903744b20fd7567a1fcefa99133b5bc
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/8c/3c7fbcd903744b20fd7567a1fcefa99133b5bc b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/8c/3c7fbcd903744b20fd7567a1fcefa99133b5bc
new file mode 100644
index 0000000..7c9f6bb
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/8c/3c7fbcd903744b20fd7567a1fcefa99133b5bc differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/9a/7df788cf800241e3bb5a849c8870f2f8259d98
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/9a/7df788cf800241e3bb5a849c8870f2f8259d98 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/9a/7df788cf800241e3bb5a849c8870f2f8259d98
new file mode 100644
index 0000000..04693ec
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/9a/7df788cf800241e3bb5a849c8870f2f8259d98 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/be/00c63250248c284b842deee5d8fb0b8132acab
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/be/00c63250248c284b842deee5d8fb0b8132acab b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/be/00c63250248c284b842deee5d8fb0b8132acab
new file mode 100644
index 0000000..2cab292
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/be/00c63250248c284b842deee5d8fb0b8132acab differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/d7/c40db3ffe2b87e96b94c280a67265c8de7a4ad
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/d7/c40db3ffe2b87e96b94c280a67265c8de7a4ad b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/d7/c40db3ffe2b87e96b94c280a67265c8de7a4ad
new file mode 100644
index 0000000..491fcfa
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/d7/c40db3ffe2b87e96b94c280a67265c8de7a4ad differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/df/30427c488aeab84b2352bdf88a3b19223f9d7a
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/df/30427c488aeab84b2352bdf88a3b19223f9d7a b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/df/30427c488aeab84b2352bdf88a3b19223f9d7a
new file mode 100644
index 0000000..8ad2f5e
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/df/30427c488aeab84b2352bdf88a3b19223f9d7a
@@ -0,0 +1,2 @@
+x��K
+�@@]�)r%�f~ ��K7� MSm+e��"��������0�*x�u6�J�-!�,���\����IX�G���6V��!� �N=�h lZĀ��Z�5;y��4���w�OO{���z�n/f��hu�s䔐	�ȈN�����ݮ��|�NG�c�E�
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e5/6a4d15295d3754310f114c86d93645308110ad
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e5/6a4d15295d3754310f114c86d93645308110ad b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e5/6a4d15295d3754310f114c86d93645308110ad
new file mode 100644
index 0000000..7d5ae85
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e5/6a4d15295d3754310f114c86d93645308110ad differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78 b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78
new file mode 100644
index 0000000..341688a
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/objects/e9/65047ad7c57865823c7d992b1d046ea66edf78 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/data/testgit.git/refs/heads/master
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/refs/heads/master b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/refs/heads/master
new file mode 100644
index 0000000..7e970a5
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/data/testgit.git/refs/heads/master
@@ -0,0 +1 @@
+1e146e67985dcd71c74de79613719bef7bddca4a

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1528fc95/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
index f0ee6a2..2972a0e 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_stats.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -1,13 +1,16 @@
 import pkg_resources
+import unittest
 
 from pylons import app_globals as g
 from pylons import tmpl_context as c
 
-from alluratest.controller import TestController
+from alluratest.controller import TestController, setup_basic_test, setup_global_objects
 from allura.tests import decorators as td
 from allura.lib import helpers as h
 from allura.model import User
+from allura import model as M
 
+from forgegit.tests import with_git
 from forgewiki import model as WM
 from forgetracker import model as TM
 
@@ -171,3 +174,40 @@ class TestStats(TestController):
         assert tickets['revoked'] == initial_tickets['revoked'] + 1
         assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
         assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 3
+
+class TestGitCommit(unittest.TestCase, TestController):
+
+    def setUp(self):
+        setup_basic_test()
+        for ep in pkg_resources.iter_entry_points("allura.stats"):
+            if ep.name.lower() == 'userstats':
+                g.statslisteners = [ep.load()().listener]
+
+        self.user = User.register(dict(username='testuser',
+            display_name='Test'),
+            make_project=False)
+        self.user.set_password('testpassword')
+        addr = M.EmailAddress.upsert('rcopeland@geek.net')
+        self.user.claim_address('rcopeland@geek.net')
+        self.setup_with_tools()
+
+    @with_git
+    @td.with_wiki
+    def setup_with_tools(self):
+        setup_global_objects()
+        h.set_context('test', 'src-git', neighborhood='Projects')
+        repo_dir = pkg_resources.resource_filename(
+            'forgeuserstats', 'tests/data')
+        c.app.repo.fs_path = repo_dir
+        c.app.repo.name = 'testgit.git'
+        self.repo = c.app.repo
+        self.repo.refresh()
+        self.rev = M.repo.Commit.query.get(_id=self.repo.heads[0]['object_id'])
+        self.rev.repo = self.repo
+
+    def test_commit(self):
+        commits = self.user.stats.getCommits()
+        assert commits['number'] == 4
+        lmcommits = self.user.stats.getLastMonthCommits()
+        assert lmcommits['number'] == 4
+

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


[40/48] git commit: [#5453] Make image assets local to tool

Posted by tv...@apache.org.
[#5453] Make image assets local to tool


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

Branch: refs/heads/si/5453
Commit: 85f83c2263797a4ec13fd5adff326961baf5f842
Parents: edb0bcf
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 5 21:44:54 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/main.py              |   10 +++++-----
 .../nf/userstats/images/stats_24.png               |  Bin 0 -> 654 bytes
 .../nf/userstats/images/stats_32.png               |  Bin 0 -> 715 bytes
 .../nf/userstats/images/stats_48.png               |  Bin 0 -> 925 bytes
 4 files changed, 5 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/85f83c22/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
index dcecdb9..acc196d 100644
--- a/ForgeUserStats/forgeuserstats/main.py
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -71,8 +71,8 @@ class UserStatsListener(EventsListener):
 
 class ForgeUserStatsApp(Application):
     __version__ = version.__version__
-    tool_label='Statistics'
-    default_mount_label='Statistics'
+    tool_label='Stats'
+    default_mount_label='Stats'
     default_mount_point='stats'
     permissions = ['configure', 'read', 'write',
                     'unmoderated_post', 'post', 'moderate', 'admin']
@@ -81,9 +81,9 @@ class ForgeUserStatsApp(Application):
     config_options = Application.config_options
     default_external_feeds = []
     icons={
-        24:'images/stats_24.png',
-        32:'images/stats_32.png',
-        48:'images/stats_48.png'
+        24:'../../tool/userstats/images/stats_24.png',
+        32:'../../tool/userstats/images/stats_32.png',
+        48:'../../tool/userstats/images/stats_48.png'
     }
     root = ForgeUserStatsController()
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/85f83c22/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_24.png
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_24.png b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_24.png
new file mode 100644
index 0000000..e3e9f5e
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_24.png differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/85f83c22/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_32.png
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_32.png b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_32.png
new file mode 100644
index 0000000..ba76a37
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_32.png differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/85f83c22/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_48.png
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_48.png b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_48.png
new file mode 100644
index 0000000..851e122
Binary files /dev/null and b/ForgeUserStats/forgeuserstats/nf/userstats/images/stats_48.png differ


[10/48] git commit: [#4808] s/Forge/Allura/g in README.markdown

Posted by tv...@apache.org.
[#4808] s/Forge/Allura/g in README.markdown

Way back before Allura was named, 'Forge' was used.  Better to just call
things Allura now.


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

Branch: refs/heads/si/5453
Commit: d882b0be57bfd93c47ad5532d2440bfd8a03edfa
Parents: f5df3c4
Author: Dave Brondsema <db...@geek.net>
Authored: Sat Mar 9 13:29:44 2013 -0500
Committer: Dave Brondsema <db...@geek.net>
Committed: Wed Mar 13 16:29:28 2013 -0400

----------------------------------------------------------------------
 README.markdown |   46 +++++++++++++++++++++++-----------------------
 1 files changed, 23 insertions(+), 23 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/d882b0be/README.markdown
----------------------------------------------------------------------
diff --git a/README.markdown b/README.markdown
index 775b97a..4ef4221 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,6 +1,6 @@
 # Sandbox Creation
 
-We'll use [VirtualBox](http://www.virtualbox.org) and [Ubuntu 12.04](http://ubuntu.com) (11.10 works too) to create a disposable sandbox for Forge development/testing.
+We'll use [VirtualBox](http://www.virtualbox.org) and [Ubuntu 12.04](http://ubuntu.com) (11.10 works too) to create a disposable sandbox for Allura development/testing.
 
 * Download and install [VirtualBox](http://www.virtualbox.org/wiki/Downloads) for your platform.
 
@@ -15,9 +15,9 @@ We'll use [VirtualBox](http://www.virtualbox.org) and [Ubuntu 12.04](http://ubun
 * Consult [available documentation](https://help.ubuntu.com/) for help installing Ubuntu.
 
 
-# Forge Installation
+# Installation
 
-Before we begin, you'll need the following additional packages in order to work with the Forge source code.
+Before we begin, you'll need the following additional packages in order to work with the Allura source code.
 
     ~$ sudo aptitude install git-core subversion python-svn
 
@@ -35,35 +35,35 @@ If you are using a different base system, make sure you have Mongo 1.8 or better
 
 ## Setting up a virtual python environment
 
-The first step to installing the Forge platform is installing a virtual environment via `virtualenv`.  This helps keep our distribution python installation clean.
+The first step to installing the Allura platform is installing a virtual environment via `virtualenv`.  This helps keep our distribution python installation clean.
 
     ~$ sudo aptitude install python-pip
     ~$ sudo pip install virtualenv
 
-Once you have virtualenv installed, you need to create a virtual environment.  We'll call our Forge environment 'anvil'.
+Once you have virtualenv installed, you need to create a virtual environment.  We'll call our Allura environment 'anvil'.
 
     ~$ virtualenv --system-site-packages anvil
 
-This gives us a nice, clean environment into which we can install all the forge dependencies.  (The site-packages flag is to include the python-svn package).  In order to use the virtual environment, you'll need to activate it.  You'll need to do this whenever you're working on the Forge codebase so you may want to consider adding it to your `~/.bashrc` file.
+This gives us a nice, clean environment into which we can install all the allura dependencies.  (The site-packages flag is to include the python-svn package).  In order to use the virtual environment, you'll need to activate it.  You'll need to do this whenever you're working on the Allura codebase so you may want to consider adding it to your `~/.bashrc` file.
 
     ~$ . anvil/bin/activate
 
-## Installing the Forge code and dependencies
+## Installing the Allura code and dependencies
 
-Now we can get down to actually getting the Forge code and dependencies downloaded and ready to go.
+Now we can get down to actually getting the Allura code and dependencies downloaded and ready to go.
 
     (anvil)~$ mkdir src
     (anvil)~$ cd src
-    (anvil)~/src$ git clone https://git-wip-us.apache.org/repos/asf/incubator-allura.git forge
+    (anvil)~/src$ git clone https://git-wip-us.apache.org/repos/asf/incubator-allura.git allura
 
 Although the application setup.py files define a number of dependencies, the `requirements.txt` files are currently the authoritative source, so we'll use those with `pip` to make sure the correct versions are installed.
 
-    (anvil)~/src$ cd forge
-    (anvil)~/src/forge$ pip install -r requirements.txt
+    (anvil)~/src$ cd allura
+    (anvil)~/src/allura$ pip install -r requirements.txt
 
 This will take a while.  If you get an error from pip, it is typically a temporary download error.  Just run the command again and it will quickly pass through the packages it already downloaded and then continue.
 
-And now to setup each of the Forge applications for development.  Because there are quite a few (at last count 15), we'll use a simple shell loop to set them up.
+And now to setup each of the Allura applications for development.  Because there are quite a few (at last count 15), we'll use a simple shell loop to set them up.
 
     for APP in Allura* Forge* NoWarnings
     do
@@ -72,7 +72,7 @@ And now to setup each of the Forge applications for development.  Because there
         popd
     done
 
-Hopefully everything completed without errors.  We'll also need to create a place for Forge to store any SCM repositories that a project might create.
+Hopefully everything completed without errors.  We'll also need to create a place for Allura to store any SCM repositories that a project might create.
 
     for SCM in git svn hg
     do
@@ -84,7 +84,7 @@ Hopefully everything completed without errors.  We'll also need to create a plac
 
 ## Initializing the environment
 
-The forge consists of several components, all of which need to be running to have full functionality.
+The Allura forge consists of several components, all of which need to be running to have full functionality.
 
 ### SOLR search and indexing server
 
@@ -94,27 +94,27 @@ We have a custom config ready for use.
     (anvil)~/src$ wget http://archive.apache.org/dist/lucene/solr/1.4.1/apache-solr-1.4.1.tgz
     (anvil)~/src$ tar xf apache-solr-1.4.1.tgz
     (anvil)~/src$ cd apache-solr-1.4.1/example/
-    (anvil)~/src/apache-solr-1.4.1/example/$ mkdir -p ~/src/forge/solr_config/conf
-    (anvil)~/src/apache-solr-1.4.1/example/$ cp solr/conf/solrconfig.xml ~/src/forge/solr_config/conf/
-    (anvil)~/src/apache-solr-1.4.1/example/$ nohup java -Dsolr.solr.home=$(cd;pwd)/src/forge/solr_config -jar start.jar > ~/logs/solr.log &
+    (anvil)~/src/apache-solr-1.4.1/example/$ mkdir -p ~/src/allura/solr_config/conf
+    (anvil)~/src/apache-solr-1.4.1/example/$ cp solr/conf/solrconfig.xml ~/src/allura/solr_config/conf/
+    (anvil)~/src/apache-solr-1.4.1/example/$ nohup java -Dsolr.solr.home=$(cd;pwd)/src/allura/solr_config -jar start.jar > ~/logs/solr.log &
 
 
-### Forge task processing
+### Allura task processing
 
 Responds to asynchronous task requests.
 
-    (anvil)~$ cd ~/src/forge/Allura
-    (anvil)~/src/forge/Allura$ nohup paster taskd development.ini > ~/logs/taskd.log &
+    (anvil)~$ cd ~/src/allura/Allura
+    (anvil)~/src/allura/Allura$ nohup paster taskd development.ini > ~/logs/taskd.log &
 
 ### TurboGears application server
 
-In order to initialize the Forge database, you'll need to run the following:
+In order to initialize the Allura database, you'll need to run the following:
 
-    (anvil)~/src/forge/Allura$ paster setup-app development.ini
+    (anvil)~/src/allura/Allura$ paster setup-app development.ini
 
 This shouldn't take too long, but it will start the taskd server doing tons of stuff in the background.  It should complete in 5-6 minutes.  Once this is done, you can start the application server.
 
-    (anvil)~/src/forge/Allura$ nohup paster serve --reload development.ini > ~/logs/tg.log &
+    (anvil)~/src/allura/Allura$ nohup paster serve --reload development.ini > ~/logs/tg.log &
 
 ## Next Steps
 


[35/48] git commit: [5453] Improved code

Posted by tv...@apache.org.
[5453] Improved code


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

Branch: refs/heads/si/5453
Commit: f689ce02f51e2d1a979c252d3f611f2c0a317faf
Parents: fa9f88b
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 16 14:33:22 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:55 2013 +0000

----------------------------------------------------------------------
 Allura/allura/templates/user_preferences.html      |    1 +
 .../forgeuserstats/templates/artifacts.html        |    2 +-
 .../forgeuserstats/templates/commits.html          |    2 +-
 ForgeUserStats/forgeuserstats/templates/index.html |    2 +-
 .../forgeuserstats/templates/tickets.html          |    2 +-
 5 files changed, 5 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f689ce02/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
index 8159f52..bd4c71e 100644
--- a/Allura/allura/templates/user_preferences.html
+++ b/Allura/allura/templates/user_preferences.html
@@ -133,6 +133,7 @@
     <a name="Statistics"></a>
     <div class="grid-20">
       <h2>Contribution statistics</h2>
+      <ul><li><a href="/userstats/{{c.user.username}}">Click here to check your personal statistics</a></li></ul>
       {{g.theme.statistics_form.display(user=c.user)}}
     </div>
   {% endif %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f689ce02/ForgeUserStats/forgeuserstats/templates/artifacts.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/artifacts.html b/ForgeUserStats/forgeuserstats/templates/artifacts.html
index d6a01d6..013c108 100644
--- a/ForgeUserStats/forgeuserstats/templates/artifacts.html
+++ b/ForgeUserStats/forgeuserstats/templates/artifacts.html
@@ -48,7 +48,7 @@
     </div>
     {% endif %}
   {% else %}
-    {% if not user.stats.visible %}
+    {% if user %}
       <h2>Statistics not available</h2>
       <div class="grid-20"> 
         This user has set his or her preferences so that personal statistics are not visible

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f689ce02/ForgeUserStats/forgeuserstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/commits.html b/ForgeUserStats/forgeuserstats/templates/commits.html
index cec0ab7..10d1c67 100644
--- a/ForgeUserStats/forgeuserstats/templates/commits.html
+++ b/ForgeUserStats/forgeuserstats/templates/commits.html
@@ -38,7 +38,7 @@
     </div>
     {% endif %}
   {% else %}
-    {% if not user.stats.visible %}
+    {% if user %}
       <h2>Statistics not available</h2>
       <div class="grid-20"> 
         This user has set his or her preferences so that personal statistics are not visible

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f689ce02/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index 4a8f504..653cd31 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -418,7 +418,7 @@
       </p>
     {% endif %}
   {% else %}
-    {% if not user.stats.visible %}
+    {% if user %}
       <h2>Statistics not available</h2>
       <div class="grid-20"> 
         This user has set his or her preferences so that personal statistics are not visible

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/f689ce02/ForgeUserStats/forgeuserstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/tickets.html b/ForgeUserStats/forgeuserstats/templates/tickets.html
index 6ee66d4..a713b25 100644
--- a/ForgeUserStats/forgeuserstats/templates/tickets.html
+++ b/ForgeUserStats/forgeuserstats/templates/tickets.html
@@ -48,7 +48,7 @@
     </div>
     {% endif %}
   {% else %}
-    {% if not user.stats.visible %}
+    {% if user %}
       <h2>Statistics not available</h2>
       <div class="grid-20"> 
         This user has set his or her preferences so that personal statistics are not visible


[29/48] git commit: [5453] Added unit tests to userstats

Posted by tv...@apache.org.
[5453] Added unit tests to userstats


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

Branch: refs/heads/si/5453
Commit: eea9aa68cc0dd8eec145926815f5c4f91b51d4b9
Parents: 5369e11
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Jan 20 16:33:25 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/contrib_stats.py               |  635 +++++++++++++++
 .../forgeuserstats/controllers/userstats.py        |    1 -
 ForgeUserStats/forgeuserstats/main.py              |   10 +-
 ForgeUserStats/forgeuserstats/model/stats.py       |  631 +--------------
 ForgeUserStats/forgeuserstats/tests/test_model.py  |  375 +++++++++
 ForgeUserStats/forgeuserstats/tests/test_stats.py  |   33 -
 6 files changed, 1029 insertions(+), 656 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
new file mode 100644
index 0000000..8a71d86
--- /dev/null
+++ b/Allura/allura/model/contrib_stats.py
@@ -0,0 +1,635 @@
+import pymongo
+from pylons import c, g, request
+
+import bson
+from ming import schema as S
+from ming import Field, Index, collection
+from ming.orm import session, state, Mapper
+from ming.orm import FieldProperty
+from ming.orm.declarative import MappedClass
+from datetime import datetime, timedelta
+import difflib
+
+from allura.model.session import main_orm_session
+from allura.lib import helpers as h
+
+class Stats(MappedClass):
+    class __mongometa__:
+        name='stats'
+        session = main_orm_session
+        unique_indexes = [ '_id']
+
+    _id=FieldProperty(S.ObjectId)
+
+    registration_date = FieldProperty(datetime)
+    general = FieldProperty([dict(
+        category = S.ObjectId,
+        messages = [dict(
+            messagetype = str,
+            created = int,
+            modified = int)],
+        tickets = dict(
+            solved = int,
+            assigned = int,
+            revoked = int,
+            totsolvingtime = int),
+        commits = [dict(
+            lines = int,
+            number = int,
+            language = S.ObjectId)])])
+
+    lastmonth=FieldProperty(dict(
+        messages=[dict(
+            datetime=datetime,
+            created=bool,
+            categories=[S.ObjectId],
+            messagetype=str)],
+        assignedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId])],
+        revokedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId])],
+        solvedtickets=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId],
+            solvingtime=int)],
+        commits=[dict(
+            datetime=datetime,
+            categories=[S.ObjectId],
+            programming_languages=[S.ObjectId],
+            lines=int)]))
+
+    def getCodeContribution(self):
+        days=(datetime.today() - self.registration_date).days
+        if not days:
+            days=1
+        for val in self['general']:
+            if val['category'] is None:
+                for commits in val['commits']:
+                    if commits['language'] is None: 
+                        if days > 30:
+                            return round(float(commits.lines)/days*30, 2)
+                        else:
+                            return float(commits.lines)
+        return 0
+
+    def getDiscussionContribution(self):
+        days=(datetime.today() - self.registration_date).days
+        if not days:
+            days=1
+        for val in self['general']:
+            if val['category'] is None:
+                for artifact in val['messages']:
+                    if artifact['messagetype'] is None: 
+                        tot = artifact.created+artifact.modified
+                        if days > 30:
+                            return round(float(tot)/days*30,2)
+                        else:
+                            return float(tot)
+        return 0
+
+    def getTicketsContribution(self):
+        for val in self['general']:
+            if val['category'] is None:
+                tickets = val['tickets']
+                if tickets.assigned == 0:
+                    return 0
+                return float(tickets.solved) / tickets.assigned
+        return 0
+
+    @classmethod
+    def getMaxAndAverageCodeContribution(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getCodeContribution() for x in lst])
+        averagecontribution=sum([x.getCodeContribution() for x in lst]) / n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageDiscussionContribution(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getDiscussionContribution() for x in lst])
+        averagecontribution=sum([x.getDiscussionContribution() for x in lst])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageTicketsSolvingPercentage(self):
+        lst = list(self.query.find())
+        n = len(lst)
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getTicketsContribution() for x in lst])
+        averagecontribution=sum([x.getTicketsContribution() for x in lst])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    def codeRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        codcontr = self.getCodeContribution()
+        upper = len([x for x in lst if x.getCodeContribution() > codcontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def discussionRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        disccontr = self.getDiscussionContribution()
+        upper=len([x for x in lst if x.getDiscussionContribution()>disccontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def ticketsRanking(self):
+        lst = list(self.query.find())
+        totn = len(lst)
+        ticketscontr = self.getTicketsContribution()
+        upper=len([x for x in lst if x.getTicketsContribution()>ticketscontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def getCommits(self, category = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None: 
+            return dict(number=0, lines=0)
+        cat = self.general[i]
+        j = getElementIndex(cat.commits, language = None)
+        if j is None:
+            return dict(number=0, lines=0)
+        return dict(
+            number=cat.commits[j]['number'], 
+            lines=cat.commits[j]['lines'])
+
+    def getArtifacts(self, category = None, art_type = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None:
+            return dict(created=0, modified=0)
+        cat = self.general[i]
+        j = getElementIndex(cat.messages, messagetype = art_type)
+        if j is None:
+            return dict(created=0, modified=0)
+        return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)
+
+    def getTickets(self, category = None):
+        i = getElementIndex(self.general, category = category)
+        if i is None:
+            return dict(
+                assigned=0,
+                solved=0,
+                revoked=0,
+                averagesolvingtime=None)
+        if self.general[i].tickets.solved > 0:
+            tot = self.general[i].tickets.totsolvingtime 
+            number = self.general[i].tickets.solved
+            average = tot / number
+        else: 
+            average = None
+        return dict(
+            assigned=self.general[i].tickets.assigned,
+            solved=self.general[i].tickets.solved,
+            revoked=self.general[i].tickets.revoked,
+            averagesolvingtime=_convertTimeDiff(average))
+
+    def getCommitsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            i = getElementIndex(entry.commits, language = None)
+            if i is None: 
+                n, lines = 0, 0
+            else: 
+                n, lines = entry.commits[i].number, entry.commits[i].lines
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(number=n, lines=lines)
+        return by_cat
+
+    #For the moment, commit stats by language are not used, since each project
+    #can be linked to more than one programming language and we don't know how
+    #to which programming language should be credited a line of code modified
+    #within a project including two or more languages.
+    def getCommitsByLanguage(self):
+        langlist = []
+        by_lang = {}
+        i = getElementIndex(self.general, category=None)
+        if i is None: 
+            return dict(number=0, lines=0)
+        return dict([(el.language, dict(lines=el.lines, number=el.number))
+                     for el in self.general[i].commits])
+
+    def getArtifactsByCategory(self, detailed=False):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            if cat != None: 
+                cat = TroveCategory.query.get(_id = cat)
+            if detailed: 
+                by_cat[cat] = entry.messages
+            else:
+                i = getElementIndex(entry.messages, messagetype=None)
+                if i is not None:
+                    by_cat[cat] = entry.messages[i]
+                else: 
+                    by_cat[cat] = dict(created=0, modified=0)
+        return by_cat
+
+    def getArtifactsByType(self, category=None):
+        i = getElementIndex(self.general, category = category)
+        if i is None: 
+            return {}
+        entry = self.general[i].messages
+        by_type = dict([(el.messagetype, dict(created=el.created,
+                                              modified=el.modified))
+                         for el in entry])
+        return by_type
+
+    def getTicketsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        by_cat = {}
+        for entry in self.general:
+            cat = entry.category
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            a, s = entry.tickets.assigned, entry.tickets.solved
+            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
+            if s:
+                average = time / s
+            else:
+                average = None
+            by_cat[cat] = dict(
+                assigned=a,
+                solved=s,
+                revoked=r, 
+                averagesolvingtime=_convertTimeDiff(average))
+        return by_cat
+
+    def getLastMonthCommits(self, category = None):
+        self.checkOldArtifacts() 
+        lineslist = [el.lines for el in self.lastmonth.commits
+                     if category in el.categories + [None]]
+        return dict(number=len(lineslist), lines=sum(lineslist))
+
+    def getLastMonthCommitsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist:
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if cat in el.categories + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(number=n, lines=lines)
+        return by_cat
+
+    def getLastMonthCommitsByLanguage(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        langlist=[el.language for el in self.general
+                  if el.language not in seen and not seen.add(el.language)]
+
+        by_lang = {}
+        for lang in langlist:
+            lineslist = [el.lines for el in self.lastmonth.commits
+                         if lang in el.programming_languages + [None]]
+            n = len(lineslist)
+            lines = sum(lineslist)
+            if lang != None:
+                lang = TroveCategory.query.get(_id = lang)
+            by_lang[lang] = dict(number=n, lines=lines)
+        return by_lang
+
+    def getLastMonthArtifacts(self, category = None, art_type = None):
+        self.checkOldArtifacts() 
+        cre, mod = reduce(
+            addtuple, 
+            [(int(el.created),1-int(el.created))
+                for el in self.lastmonth.messages
+                if (category is None or category in el.categories) and 
+                (el.messagetype == art_type or art_type is None)], 
+            (0,0))
+        return dict(created=cre, modified=mod)
+
+    def getLastMonthArtifactsByType(self, category = None):
+        self.checkOldArtifacts()
+        seen = set()
+        types=[el.messagetype for el in self.lastmonth.messages
+               if el.messagetype not in seen and not seen.add(el.messagetype)]
+
+        by_type = {}
+        for t in types:
+            cre, mod = reduce(
+                addtuple, 
+                [(int(el.created),1-int(el.created))
+                 for el in self.lastmonth.messages
+                 if el.messagetype == t and
+                 category in [None]+el.categories],
+                (0,0))
+            by_type[t] = dict(created=cre, modified=mod)
+        return by_type
+
+    def getLastMonthArtifactsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts() 
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+
+        by_cat = {}
+        for cat in catlist:
+            cre, mod = reduce(
+                addtuple, 
+                [(int(el.created),1-int(el.created))
+                 for el in self.lastmonth.messages 
+                 if cat in el.categories + [None]], (0,0))
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            by_cat[cat] = dict(created=cre, modified=mod)
+        return by_cat
+
+    def getLastMonthTickets(self, category = None):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts()
+        a = len([el for el in self.lastmonth.assignedtickets
+                 if category in el.categories + [None]])
+        r = len([el for el in self.lastmonth.revokedtickets
+                 if category in el.categories + [None]])
+        s, time = reduce(
+            addtuple, 
+            [(1, el.solvingtime)
+             for el in self.lastmonth.solvedtickets
+             if category in el.categories + [None]],
+            (0,0))
+        if category!=None:
+            category = TroveCategory.query.get(_id=category)
+        if s > 0:
+            time = time / s
+        else:
+            time = None
+        return dict(
+            assigned=a,
+            revoked=r,
+            solved=s, 
+            averagesolvingtime=_convertTimeDiff(time))
+        
+    def getLastMonthTicketsByCategory(self):
+        from allura.model.project import TroveCategory
+
+        self.checkOldArtifacts()
+        seen = set()
+        catlist=[el.category for el in self.general
+                 if el.category not in seen and not seen.add(el.category)]
+        by_cat = {}
+        for cat in catlist:
+            a = len([el for el in self.lastmonth.assignedtickets
+                     if cat in el.categories + [None]])
+            r = len([el for el in self.lastmonth.revokedtickets
+                     if cat in el.categories + [None]])
+            s, time = reduce(addtuple, [(1, el.solvingtime)
+                                        for el in self.lastmonth.solvedtickets
+                                        if cat in el.categories+[None]],(0,0))
+            if cat != None:
+                cat = TroveCategory.query.get(_id = cat)
+            if s > 0: 
+                time = time / s
+            else:
+                time = None
+            by_cat[cat] = dict(
+                assigned=a,
+                revoked=r,
+                solved=s, 
+                averagesolvingtime=_convertTimeDiff(time))
+        return by_cat
+        
+    def checkOldArtifacts(self):
+        now = datetime.utcnow()
+        for m in self.lastmonth.messages:
+            if now - m.datetime > timedelta(30):
+                self.lastmonth.messages.remove(m)
+        for t in self.lastmonth.assignedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.assignedtickets.remove(t)
+        for t in self.lastmonth.revokedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.revokedtickets.remove(t)
+        for t in self.lastmonth.solvedtickets:
+            if now - t.datetime > timedelta(30):
+                self.lastmonth.solvedtickets.remove(t)
+        for c in self.lastmonth.commits:
+            if now - c.datetime > timedelta(30):
+                self.lastmonth.commits.remove(c)
+
+    def addNewArtifact(self, art_type, art_datetime, project):
+        self._updateArtifactsStats(art_type, art_datetime, project, "created")
+
+    def addModifiedArtifact(self, art_type, art_datetime, project):
+        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
+
+    def addAssignedTicket(self, ticket_datetime, project):
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'assigned')
+        self.lastmonth.assignedtickets.append(
+            dict(datetime=ticket_datetime, categories=topics))
+
+    def addRevokedTicket(self, ticket_datetime, project):
+        topics = [t for t in project.trove_topic if t]
+        self._updateTicketsStats(topics, 'revoked')
+        self.lastmonth.revokedtickets.append(
+            dict(datetime=ticket_datetime, categories=topics))
+        self.checkOldArtifacts()
+
+    def addClosedTicket(self, open_datetime, close_datetime, project):
+        topics = [t for t in project.trove_topic if t]
+        s_time=int((close_datetime-open_datetime).total_seconds())
+        self._updateTicketsStats(topics, 'solved', s_time = s_time)
+        self.lastmonth.solvedtickets.append(dict(
+            datetime=close_datetime,
+            categories=topics,
+            solvingtime=s_time))
+        self.checkOldArtifacts()
+
+    def addCommit(self, newcommit, commit_datetime, project):
+        def _computeLines(newblob, oldblob = None):
+            if oldblob:
+                listold = list(oldblob)
+            else:
+                listold = []
+            if newblob:
+                listnew = list(newblob)
+            else:
+                listnew = []
+
+            if oldblob is None:
+                lines = len(listnew)
+            elif newblob and newblob.has_html_view:
+                diff = difflib.unified_diff(
+                    listold, listnew,
+                    ('old' + oldblob.path()).encode('utf-8'),
+                    ('new' + newblob.path()).encode('utf-8'))
+                lines = len([l for l in diff if len(l) > 0 and l[0] == '+'])-1
+            else:
+                lines = 0
+            return lines
+
+        def _addCommitData(stats, topics, languages, lines):          
+            lt = topics + [None]
+            ll = languages + [None]
+            for t in lt:
+                i = getElementIndex(stats.general, category=t) 
+                if i is None:
+                    newstats = dict(
+                        category=t,
+                        commits=[],
+                        messages=dict(
+                            assigned=0,
+                            solved=0,
+                            revoked=0,
+                            totsolvingtime=0),
+                        tickets=[])   
+                    stats.general.append(newstats)
+                    i = getElementIndex(stats.general, category=t)
+                for lang in ll:
+                    j = getElementIndex(
+                        stats.general[i]['commits'], language=lang)
+                    if j is None:
+                        stats.general[i]['commits'].append(dict(
+                            language=lang, lines=lines, number=1))
+                    else:
+                        stats.general[i]['commits'][j].lines += lines
+                        stats.general[i]['commits'][j].number += 1
+
+        topics = [t for t in project.trove_topic if t]
+        languages = [l for l in project.trove_language if l]
+
+        d = newcommit.diffs
+        if len(newcommit.parent_ids) > 0:
+            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
+
+        totlines = 0
+        for changed in d.changed:
+            newblob = newcommit.tree.get_blob_by_path(changed)
+            oldblob = oldcommit.tree.get_blob_by_path(changed)
+            totlines+=_computeLines(newblob, oldblob)
+
+        for copied in d.copied:
+            newblob = newcommit.tree.get_blob_by_path(copied['new'])
+            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
+            totlines+=_computeLines(newblob, oldblob)
+
+        for added in d.added:
+            newblob = newcommit.tree.get_blob_by_path(added)
+            totlines+=_computeLines(newblob)
+
+        _addCommitData(self, topics, languages, totlines)
+
+        self.lastmonth.commits.append(dict(
+            datetime=commit_datetime, 
+            categories=topics, 
+            programming_languages=languages,
+            lines=totlines))
+        self.checkOldArtifacts()
+
+    def _updateArtifactsStats(self, art_type, art_datetime, project, action):
+        if action not in ['created', 'modified']: 
+            return
+        topics = [t for t in project.trove_topic if t]
+        lt = [None] + topics
+        for mtype in [None, art_type]:
+            for t in lt:
+                i = getElementIndex(self.general, category = t)
+                if i is None:
+                    msg = dict(
+                        category=t,
+                        commits=[],
+                        tickets=dict(
+                            solved=0,
+                            assigned=0,
+                            revoked=0,
+                            totsolvingtime=0),
+                        messages=[])
+                    self.general.append(msg)
+                    i = getElementIndex(self.general, category = t)
+                j = getElementIndex(
+                    self.general[i]['messages'], messagetype=mtype)
+                if j is None:
+                    entry = dict(messagetype=mtype, created=0, modified=0)
+                    entry[action] += 1
+                    self.general[i]['messages'].append(entry)
+                else:
+                    self.general[i]['messages'][j][action] += 1
+
+        self.lastmonth.messages.append(dict(
+            datetime=art_datetime,
+            created=(action == 'created'),
+            categories=topics,
+            messagetype=art_type))
+        self.checkOldArtifacts() 
+
+    def _updateTicketsStats(self, topics, action, s_time = None):
+        if action not in ['solved', 'assigned', 'revoked']:
+            return
+        lt = topics + [None]
+        for t in lt:
+            i = getElementIndex(self.general, category = t)
+            if i is None:
+                stats = dict(
+                    category=t,
+                    commits=[],
+                    tickets=dict(
+                        solved=0,
+                        assigned=0,
+                        revoked=0,
+                        totsolvingtime=0),
+                    messages=[])
+                self.general.append(stats)
+                i = getElementIndex(self.general, category = t)
+            self.general[i]['tickets'][action] += 1 
+            if action == 'solved': 
+                self.general[i]['tickets']['totsolvingtime']+=s_time
+
+def getElementIndex(el_list, **kw):
+    for i in range(len(el_list)):
+        for k in kw:
+            if el_list[i].get(k) != kw[k]:
+                break
+        else:
+            return i
+    return None
+
+def addtuple(l1, l2):
+    a, b = l1
+    x, y = l2
+    return (a+x, b+y)
+
+def _convertTimeDiff(int_seconds):
+    if int_seconds is None:
+        return None
+    diff = timedelta(seconds = int_seconds)
+    days, seconds = diff.days, diff.seconds
+    hours = seconds / 3600
+    seconds = seconds % 3600
+    minutes = seconds / 60
+    seconds = seconds % 60
+    return dict(
+        days=days, 
+        hours=hours, 
+        minutes=minutes,
+        seconds=seconds)
+
+Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index 2bfaf82..fe14449 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -228,7 +228,6 @@ def _getDataForCategory(category, stats):
             solved='n/a',
             averagesolvingtime='n/a')
         for key in artifacts_by_type:
-            value = artifacts_by_type[key]
             artifacts_by_type[key]['pmcreated'] = 'n/a'
             artifacts_by_type[key]['pmmodified']= 'n/a'
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
index 43ca2f3..8eeb113 100644
--- a/ForgeUserStats/forgeuserstats/main.py
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -32,25 +32,25 @@ class UserStatsListener(EventsListener):
             stats = UserStats.create(user)
 
         if event_type == "assigned": 
-            stats.addAssignedTicket(ticket, project)
+            stats.addAssignedTicket(ticket.mod_date, project)
         elif event_type == "revoked":
-            stats.addRevokedTicket(ticket, project)
+            stats.addRevokedTicket(ticket.mod_date, project)
         elif event_type == "closed":
-            stats.addClosedTicket(ticket, project)
+            stats.addClosedTicket(ticket.created_date,ticket.mod_date,project)
 
     def newCommit(self, newcommit, project, user):
         stats = user.stats
         if not stats:
             stats = UserStats.create(user)
 
-        stats.addCommit(newcommit, project)
+        stats.addCommit(newcommit, datetime.utcnow(), project)
 
     def addUserLogin(self, user):
         stats = user.stats
         if not stats:
             stats = UserStats.create(user)
 
-        stats.addLogin()
+        stats.addLogin(datetime.utcnow())
 
     def newOrganization(self, organization):
         pass

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 2f5e1b9..8575e79 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -1,67 +1,20 @@
-import pymongo
-from pylons import c, g, request
-
-import bson
+from ming.orm import FieldProperty
 from ming import schema as S
-from ming import Field, Index, collection
-from ming.orm import session, state, Mapper
-from ming.orm import FieldProperty, RelationProperty, ForeignIdProperty
-from ming.orm.declarative import MappedClass
 from datetime import datetime, timedelta
-import difflib
+from ming.orm import session, Mapper
 
 from allura.model.session import main_orm_session
-from allura.lib import helpers as h
+from allura.model.contrib_stats import Stats
 
-class UserStats(MappedClass):
+class UserStats(Stats):
     class __mongometa__:
         name='userstats'
         session = main_orm_session
         unique_indexes = [ '_id', 'user_id']
 
-    _id=FieldProperty(S.ObjectId)
-
-    registration_date = FieldProperty(datetime)
     tot_logins_count = FieldProperty(int, if_missing = 0)
     last_login = FieldProperty(datetime)
-    general = FieldProperty([dict(
-        category = S.ObjectId,
-        messages = [dict(
-            messagetype = str,
-            created = int,
-            modified = int)],
-        tickets = dict(
-            solved = int,
-            assigned = int,
-            revoked = int,
-            totsolvingtime = int),
-        commits = [dict(
-            lines = int,
-            number = int,
-            language = S.ObjectId)])])
-
-    lastmonth=FieldProperty(dict(
-        logins=[datetime],
-            messages=[dict(
-            datetime=datetime,
-            created=bool,
-            categories=[S.ObjectId],
-            messagetype=str)],
-        assignedtickets=[dict(
-            datetime=datetime,
-            categories=[S.ObjectId])],
-        revokedtickets=[dict(
-            datetime=datetime,
-            categories=[S.ObjectId])],
-        solvedtickets=[dict(
-            datetime=datetime,
-            categories=[S.ObjectId],
-            solvingtime=int)],
-        commits=[dict(
-            datetime=datetime,
-            categories=[S.ObjectId],
-            programming_languages=[S.ObjectId],
-            lines=int)]))
+    lastmonthlogins=FieldProperty([datetime])
     user_id = FieldProperty(S.ObjectId)
 
     @classmethod
@@ -76,578 +29,22 @@ class UserStats(MappedClass):
         session(user).flush(user)
         return stats
 
-    def getCodeContribution(self):
-        days=(datetime.today() - self.registration_date).days
-        if not days:
-            days=1
-        for val in self['general']:
-            if val['category'] is None:
-                for commits in val['commits']:
-                    if commits['language'] is None: 
-                        if days > 30:
-                            return round(float(commits.lines)/days*30, 2)
-                        else:
-                            return float(commits.lines)
-        return 0
-
-    def getDiscussionContribution(self):
-        days=(datetime.today() - self.registration_date).days
-        if not days:
-            days=1
-        for val in self['general']:
-            if val['category'] is None:
-                for artifact in val['messages']:
-                    if artifact['messagetype'] is None: 
-                        tot = artifact.created+artifact.modified
-                        if days > 30:
-                            return round(float(tot)/days*30,2)
-                        else:
-                            return float(tot)
-        return 0
-
-    def getTicketsContribution(self):
-        for val in self['general']:
-            if val['category'] is None:
-                tickets = val['tickets']
-                if tickets.assigned == 0:
-                    return 0
-                return float(tickets.solved) / tickets.assigned
-        return 0
-
-    @classmethod
-    def getMaxAndAverageCodeContribution(self):
-        lst = list(self.query.find())
-        n = len(lst)
-        if n == 0:
-            return 0, 0
-        maxcontribution=max([x.getCodeContribution() for x in lst])
-        averagecontribution=sum([x.getCodeContribution() for x in lst]) / n
-        return maxcontribution, round(averagecontribution, 2)
-
-    @classmethod
-    def getMaxAndAverageDiscussionContribution(self):
-        lst = list(self.query.find())
-        n = len(lst)
-        if n == 0:
-            return 0, 0
-        maxcontribution=max([x.getDiscussionContribution() for x in lst])
-        averagecontribution=sum([x.getDiscussionContribution() for x in lst])/n
-        return maxcontribution, round(averagecontribution, 2)
-
-    @classmethod
-    def getMaxAndAverageTicketsSolvingPercentage(self):
-        lst = list(self.query.find())
-        n = len(lst)
-        if n == 0:
-            return 0, 0
-        maxcontribution=max([x.getTicketsContribution() for x in lst])
-        averagecontribution=sum([x.getTicketsContribution() for x in lst])/n
-        return maxcontribution, round(averagecontribution, 2)
-
-    def codeRanking(self):
-        lst = list(self.query.find())
-        totn = len(lst)
-        codcontr = self.getCodeContribution()
-        upper = len([x for x in lst if x.getCodeContribution() > codcontr])
-        return round((totn - upper) * 100.0 / totn, 2)
-
-    def discussionRanking(self):
-        lst = list(self.query.find())
-        totn = len(lst)
-        disccontr = self.getDiscussionContribution()
-        upper=len([x for x in lst if x.getDiscussionContribution()>disccontr])
-        return round((totn - upper) * 100.0 / totn, 2)
-
-    def ticketsRanking(self):
-        lst = list(self.query.find())
-        totn = len(lst)
-        ticketscontr = self.getTicketsContribution()
-        upper=len([x for x in lst if x.getTicketsContribution()>ticketscontr])
-        return round((totn - upper) * 100.0 / totn, 2)
-
-    def getCommits(self, category = None):
-        i = getElementIndex(self.general, category = category)
-        if i is None: 
-            return dict(number=0, lines=0)
-        cat = self.general[i]
-        j = getElementIndex(cat.commits, language = None)
-        if j is None:
-            return dict(number=0, lines=0)
-        return dict(
-            number=cat.commits[j]['number'], 
-            lines=cat.commits[j]['lines'])
-
-    def getArtifacts(self, category = None, art_type = None):
-        i = getElementIndex(self.general, category = category)
-        if i is None:
-            return dict(created=0, modified=0)
-        cat = self.general[i]
-        j = getElementIndex(cat.messages, messagetype = art_type)
-        if j is None:
-            return dict(created=0, modified=0)
-        return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)
-
-    def getTickets(self, category = None):
-        i = getElementIndex(self.general, category = category)
-        if i is None:
-            return dict(
-                assigned=0,
-                solved=0,
-                revoked=0,
-                averagesolvingtime=None)
-        if self.general[i].tickets.solved > 0:
-            tot = self.general[i].tickets.totsolvingtime 
-            number = self.general[i].tickets.solved
-            average = tot / number
-        else: 
-            average = None
-        return dict(
-            assigned=self.general[i].tickets.assigned,
-            solved=self.general[i].tickets.solved,
-            revoked=self.general[i].tickets.revoked,
-            averagesolvingtime=_convertTimeDiff(average))
-
-    def getCommitsByCategory(self):
-        from allura.model.project import TroveCategory
-
-        by_cat = {}
-        for entry in self.general:
-            cat = entry.category
-            i = getElementIndex(entry.commits, language = None)
-            if i is None: 
-                n, lines = 0, 0
-            else: 
-                n, lines = entry.commits[i].number, entry.commits[i].lines
-            if cat != None:
-                cat = TroveCategory.query.get(_id = cat)
-            by_cat[cat] = dict(number=n, lines=lines)
-        return by_cat
-
-    def getCommitsByLanguage(self):
-        langlist = []
-        by_lang = {}
-        i = getElementIndex(self.general, category=None)
-        if i is None: 
-            return dict(number=0, lines=0)
-        return dict([(el.language, dict(lines=el.lines, number=el.number))
-                     for el in self.general[i].commits])
-
-    def getArtifactsByCategory(self, detailed=False):
-        from allura.model.project import TroveCategory
-
-        by_cat = {}
-        for entry in self.general:
-            cat = entry.category
-            if cat != None: 
-                cat = TroveCategory.query.get(_id = cat)
-            if detailed: 
-                by_cat[cat] = entry.messages
-            else:
-                i = getElementIndex(entry.messages, messagetype=None)
-                if i is not None:
-                    by_cat[cat] = entry.messages[i]
-                else: 
-                    by_cat[cat] = dict(created=0, modified=0)
-        return by_cat
-
-    def getArtifactsByType(self, category=None):
-        i = getElementIndex(self.general, category = category)
-        if i is None: 
-            return {}
-        entry = self.general[i].messages
-        by_type = dict([(el.messagetype, dict(created=el.created,
-                                              modified=el.modified))
-                         for el in entry])
-        return by_type
-
-    def getTicketsByCategory(self):
-        from allura.model.project import TroveCategory
-
-        by_cat = {}
-        for entry in self.general:
-            cat = entry.category
-            if cat != None:
-                cat = TroveCategory.query.get(_id = cat)
-            a, s = entry.tickets.assigned, entry.tickets.solved
-            r, time = entry.tickets.solved, entry.tickets.totsolvingtime
-            if s:
-                average = time / s
-            else:
-                average = None
-            by_cat[cat] = dict(
-                assigned=a,
-                solved=s,
-                revoked=r, 
-                averagesolvingtime=_convertTimeDiff(average))
-        return by_cat
-
-    def getLastMonthCommits(self, category = None):
-        self.checkOldArtifacts() 
-        lineslist = [el.lines for el in self.lastmonth.commits
-                     if category in el.categories + [None]]
-        return dict(number=len(lineslist), lines=sum(lineslist))
-
-    def getLastMonthCommitsByCategory(self):
-        from allura.model.project import TroveCategory
-
-        self.checkOldArtifacts() 
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-
-        by_cat = {}
-        for cat in catlist:
-            lineslist = [el.lines for el in self.lastmonth.commits
-                         if cat in el.categories + [None]]
-            n = len(lineslist)
-            lines = sum(lineslist)
-            if cat != None:
-                cat = TroveCategory.query.get(_id = cat)
-            by_cat[cat] = dict(number=n, lines=lines)
-        return by_cat
-
-    def getLastMonthCommitsByLanguage(self):
-        from allura.model.project import TroveCategory
-
-        self.checkOldArtifacts() 
-        seen = set()
-        langlist=[el.language for el in self.general
-                  if el.language not in seen and not seen.add(el.language)]
-
-        by_lang = {}
-        for lang in langlist:
-            lineslist = [el.lines for el in self.lastmonth.commits
-                         if lang in el.programming_languages + [None]]
-            n = len(lineslist)
-            lines = sum(lineslist)
-            if lang != None:
-                lang = TroveCategory.query.get(_id = lang)
-            by_lang[lang] = dict(number=n, lines=lines)
-        return by_lang
-
-    def getLastMonthArtifacts(self, category = None):
-        self.checkOldArtifacts() 
-        cre, mod = reduce(addtuple, [(int(el.created),1-int(el.created))
-                                     for el in self.lastmonth.messages
-                                     if category is None or 
-                                        category in el.categories], (0,0))
-        return dict(created=cre, modified=mod)
-
-    def getLastMonthArtifactsByType(self, category = None):
-        self.checkOldArtifacts()
-        seen = set()
-        types=[el.messagetype for el in self.lastmonth.messages
-               if el.messagetype not in seen and not seen.add(el.messagetype)]
-
-        by_type = {}
-        for t in types:
-            cre, mod = reduce(
-                addtuple, 
-                [(int(el.created),1-int(el.created))
-                 for el in self.lastmonth.messages
-                 if el.messagetype == t and
-                 category in [None]+el.categories],
-                (0,0))
-            by_type[t] = dict(created=cre, modified=mod)
-        return by_type
-
-    def getLastMonthArtifactsByCategory(self):
-        from allura.model.project import TroveCategory
-
-        self.checkOldArtifacts() 
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-
-        by_cat = {}
-        for cat in catlist:
-            cre, mod = reduce(
-                addtuple, 
-                [(int(el.created),1-int(el.created))
-                 for el in self.lastmonth.messages 
-                 if cat in el.categories + [None]], (0,0))
-            if cat != None:
-                cat = TroveCategory.query.get(_id = cat)
-            by_cat[cat] = dict(created=cre, modified=mod)
-        return by_cat
-
-    def getLastMonthTickets(self, category = None):
-        from allura.model.project import TroveCategory
-
-        self.checkOldArtifacts()
-        a = len([el for el in self.lastmonth.assignedtickets
-                 if category in el.categories + [None]])
-        r = len([el for el in self.lastmonth.revokedtickets
-                 if category in el.categories + [None]])
-        s, time = reduce(
-            addtuple, 
-            [(1, el.solvingtime)
-             for el in self.lastmonth.solvedtickets
-             if category in el.categories + [None]],
-            (0,0))
-        if category!=None:
-            category = TroveCategory.query.get(_id=category)
-        if s > 0:
-            time = time / s
-        else:
-            time = None
-        return dict(
-            assigned=a,
-            revoked=r,
-            solved=s, 
-            averagesolvingtime=_convertTimeDiff(time))
-        
-    def getLastMonthTicketsByCategory(self):
-        from allura.model.project import TroveCategory
-
-        self.checkOldArtifacts()
-        seen = set()
-        catlist=[el.category for el in self.general
-                 if el.category not in seen and not seen.add(el.category)]
-        by_cat = {}
-        for cat in catlist:
-            a = len([el for el in self.lastmonth.assignedtickets
-                     if cat in el.categories + [None]])
-            r = len([el for el in self.lastmonth.revokedtickets
-                     if cat in el.categories + [None]])
-            s, time = reduce(addtuple, [(1, el.solvingtime)
-                                        for el in self.lastmonth.solvedtickets
-                                        if cat in el.categories+[None]],(0,0))
-            if cat != None:
-                cat = TroveCategory.query.get(_id = cat)
-            if s > 0: 
-                time = time / s
-            else:
-                time = None
-            by_cat[cat] = dict(
-                assigned=a,
-                revoked=r,
-                solved=s, 
-                averagesolvingtime=_convertTimeDiff(time))
-        return by_cat
-        
     def getLastMonthLogins(self):
         self.checkOldArtifacts()
-        return len(self.lastmonth.logins)
+        return len(self.lastmonthlogins)
 
     def checkOldArtifacts(self):
+        super(UserStats, self).checkOldArtifacts()
         now = datetime.utcnow()
-        for m in self.lastmonth.messages:
-            if now - m.datetime > timedelta(30):
-                self.lastmonth.messages.remove(m)
-        for t in self.lastmonth.assignedtickets:
-            if now - t.datetime > timedelta(30):
-                self.lastmonth.assignedtickets.remove(t)
-        for t in self.lastmonth.revokedtickets:
-            if now - t.datetime > timedelta(30):
-                self.lastmonth.revokedtickets.remove(t)
-        for t in self.lastmonth.solvedtickets:
-            if now - t.datetime > timedelta(30):
-                self.lastmonth.solvedtickets.remove(t)
-
-    def addNewArtifact(self, art_type, art_datetime, project):
-        self._updateArtifactsStats(art_type, art_datetime, project, "created")
-
-    def addModifiedArtifact(self, art_type, art_datetime, project):
-        self._updateArtifactsStats(art_type, art_datetime, project, "modified")
-
-    def addAssignedTicket(self, ticket, project):
-        topics = [t for t in project.trove_topic if t]
-        self._updateTicketsStats(topics, 'assigned')
-        self.lastmonth.assignedtickets.append(
-            dict(datetime=ticket.mod_date, categories=topics))
-
-    def addRevokedTicket(self, ticket, project):
-        topics = [t for t in project.trove_topic if t]
-        self._updateTicketsStats(topics, 'revoked')
-        self.lastmonth.revokedtickets.append(
-            dict(datetime=ticket.mod_date, categories=topics))
-        self.checkOldArtifacts()
-
-    def addClosedTicket(self, ticket, project):
-        topics = [t for t in project.trove_topic if t]
-        s_time=int((datetime.utcnow()-ticket.created_date).total_seconds())
-        self._updateTicketsStats(topics, 'solved', s_time = s_time)
-        self.lastmonth.solvedtickets.append(dict(
-            datetime=ticket.mod_date,
-            categories=topics,
-            solvingtime=s_time))
-        self.checkOldArtifacts()
-
-    def addCommit(self, newcommit, project):
-        def _computeLines(newblob, oldblob = None):
-            if oldblob:
-                listold = list(oldblob)
-            else:
-                listold = []
-            if newblob:
-                listnew = list(newblob)
-            else:
-                listnew = []
-
-            if oldblob is None:
-                lines = len(listnew)
-            elif newblob and newblob.has_html_view:
-                diff = difflib.unified_diff(
-                    listold, listnew,
-                    ('old' + oldblob.path()).encode('utf-8'),
-                    ('new' + newblob.path()).encode('utf-8'))
-                lines = len([l for l in diff if len(l) > 0 and l[0] == '+'])-1
-            else:
-                lines = 0
-            return lines
-
-        def _addCommitData(stats, topics, languages, lines):          
-            lt = topics + [None]
-            ll = languages + [None]
-            for t in lt:
-                i = getElementIndex(stats.general, category=t) 
-                if i is None:
-                    newstats = dict(
-                        category=t,
-                        commits=[],
-                        messages=dict(
-                            assigned=0,
-                            solved=0,
-                            revoked=0,
-                            totsolvingtime=0),
-                        tickets=[])   
-                    stats.general.append(newstats)
-                    i = getElementIndex(stats.general, category=t)
-                for lang in ll:
-                    j = getElementIndex(
-                        stats.general[i]['commits'], language=lang)
-                    if j is None:
-                        stats.general[i]['commits'].append(dict(
-                            language=lang, lines=lines, number=1))
-                    else:
-                        stats.general[i]['commits'][j].lines += lines
-                        stats.general[i]['commits'][j].number += 1
-
-        topics = [t for t in project.trove_topic if t]
-        languages = [l for l in project.trove_language if l]
-        now = datetime.utcnow()
-
-        d = newcommit.diffs
-        if len(newcommit.parent_ids) > 0:
-            oldcommit = newcommit.repo.commit(newcommit.parent_ids[0])
-
-        totlines = 0
-        for changed in d.changed:
-            newblob = newcommit.tree.get_blob_by_path(changed)
-            oldblob = oldcommit.tree.get_blob_by_path(changed)
-            totlines+=_computeLines(newblob, oldblob)
-
-        for copied in d.copied:
-            newblob = newcommit.tree.get_blob_by_path(copied['new'])
-            oldblob = oldcommit.tree.get_blob_by_path(copied['old'])
-            totlines+=_computeLines(newblob, oldblob)
-
-        for added in d.added:
-            newblob = newcommit.tree.get_blob_by_path(added)
-            totlines+=_computeLines(newblob)
-
-        _addCommitData(self, topics, languages, totlines)
+        for l in self.lastmonthlogins:
+            if now - l > timedelta(30):
+                self.lastmonthlogins.remove(l)
 
-        self.lastmonth.commits.append(dict(
-            datetime=now, 
-            categories=topics, 
-            programming_languages=languages,
-            lines=totlines))
-        self.checkOldArtifacts()
-
-    def addLogin(self):
-        now = datetime.utcnow()
-        self.last_login = now
+    def addLogin(self, login_datetime):
+        if (not self.last_login) or (login_datetime > self.last_login):
+            self.last_login = login_datetime
         self.tot_logins_count += 1
-        self.lastmonth.logins.append(now)
+        self.lastmonthlogins.append(login_datetime)
         self.checkOldArtifacts()
         
-    def _updateArtifactsStats(self, art_type, art_datetime, project, action):
-        if action not in ['created', 'modified']: 
-            return
-        topics = [t for t in project.trove_topic if t]
-        lt = [None] + topics
-        for mtype in [None, art_type]:
-            for t in lt:
-                i = getElementIndex(self.general, category = t)
-                if i is None:
-                    msg = dict(
-                        category=t,
-                        commits=[],
-                        tickets=dict(
-                            solved=0,
-                            assigned=0,
-                            revoked=0,
-                            totsolvingtime=0),
-                        messages=[])
-                    self.general.append(msg)
-                    i = getElementIndex(self.general, category = t)
-                j = getElementIndex(
-                    self.general[i]['messages'], messagetype=mtype)
-                if j is None:
-                    entry = dict(messagetype=mtype, created=0, modified=0)
-                    entry[action] += 1
-                    self.general[i]['messages'].append(entry)
-                else:
-                    self.general[i]['messages'][j][action] += 1
-
-        self.lastmonth.messages.append(dict(
-            datetime=art_datetime,
-            created=(action == 'created'),
-            categories=topics,
-            messagetype=art_type))
-        self.checkOldArtifacts() 
-
-    def _updateTicketsStats(self, topics, action, s_time = None):
-        if action not in ['solved', 'assigned', 'revoked']:
-            return
-        lt = topics + [None]
-        for t in lt:
-            i = getElementIndex(self.general, category = t)
-            if i is None:
-                stats = dict(
-                    category=t,
-                    commits=[],
-                    tickets=dict(
-                        solved=0,
-                        assigned=0,
-                        revoked=0,
-                        totsolvingtime=0),
-                    messages=[])
-                self.general.append(stats)
-                i = getElementIndex(self.general, category = t)
-            self.general[i]['tickets'][action] += 1 
-            if action == 'solved': 
-                self.general[i]['tickets']['totsolvingtime']+=s_time
-
-def getElementIndex(el_list, **kw):
-    for i in range(len(el_list)):
-        for k in kw:
-            if el_list[i].get(k) != kw[k]:
-                break
-        else:
-            return i
-    return None
-
-def addtuple(l1, l2):
-    a, b = l1
-    x, y = l2
-    return (a+x, b+y)
-
-def _convertTimeDiff(int_seconds):
-    if int_seconds is None:
-        return None
-    diff = timedelta(seconds = int_seconds)
-    days, seconds = diff.days, diff.seconds
-    hours = seconds / 3600
-    seconds = seconds % 3600
-    minutes = seconds / 60
-    seconds = seconds % 60
-    return dict(
-        days=days, 
-        hours=hours, 
-        minutes=minutes,
-        seconds=seconds)
-
 Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/ForgeUserStats/forgeuserstats/tests/test_model.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_model.py b/ForgeUserStats/forgeuserstats/tests/test_model.py
new file mode 100644
index 0000000..3a9fcde
--- /dev/null
+++ b/ForgeUserStats/forgeuserstats/tests/test_model.py
@@ -0,0 +1,375 @@
+import pkg_resources
+import unittest
+from datetime import datetime, timedelta
+
+from pylons import tmpl_context as c
+
+from alluratest.controller import setup_basic_test, setup_global_objects
+from allura.tests import decorators as td
+from allura.model import User, Project, TroveCategory
+from allura import model as M
+
+from forgegit.tests import with_git
+
+class TestUserStats(unittest.TestCase):
+
+    def setUp(self):
+        from allura.model import User
+
+        setup_basic_test()
+        setup_global_objects()
+        self.user = User.register(dict(username='test-new-user',
+            display_name='Test Stats'),
+            make_project=False)
+
+    def test_init_values(self):
+        artifacts = self.user.stats.getArtifacts()
+        tickets = self.user.stats.getTickets()
+        commits = self.user.stats.getCommits()
+        assert self.user.stats.tot_logins_count == 0
+        assert artifacts['created'] == 0
+        assert artifacts['modified'] == 0
+        assert tickets['assigned'] == 0
+        assert tickets['solved'] == 0
+        assert tickets['revoked'] == 0
+        assert tickets['averagesolvingtime'] is None
+        assert commits['number'] == 0
+        assert commits['lines'] == 0
+
+        lmartifacts = self.user.stats.getLastMonthArtifacts()
+        lmtickets = self.user.stats.getLastMonthTickets()
+        lmcommits = self.user.stats.getLastMonthCommits()
+        assert self.user.stats.getLastMonthLogins() == 0
+        assert lmartifacts['created'] == 0
+        assert lmartifacts['modified'] == 0
+        assert lmtickets['assigned'] == 0
+        assert lmtickets['solved'] == 0
+        assert lmtickets['revoked'] == 0
+        assert lmtickets['averagesolvingtime'] is None
+        assert lmcommits['number'] == 0
+        assert lmcommits['lines'] == 0
+
+    @td.with_user_project('test-new-user')
+    def test_create_artifact_stats(self):
+        p = Project.query.get(shortname='u/test-new-user')
+        topic = TroveCategory.query.get(shortname='scientific')
+
+        init_lm_art = self.user.stats.getLastMonthArtifacts()
+        init_art = self.user.stats.getArtifacts()
+        init_art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        init_art_by_type = self.user.stats.getArtifactsByType()
+        init_lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+        init_art_sci = self.user.stats.getArtifacts(category=topic._id)
+
+        self.user.stats.addNewArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+
+        assert lm_art['created'] == init_lm_art['created'] + 1
+        assert lm_art['modified'] == init_lm_art['modified']
+        assert artifacts['created'] == init_art['created'] + 1
+        assert artifacts['modified'] == init_art['modified']
+        assert art_wiki['created'] == init_art_wiki['created'] + 1
+        assert art_wiki['modified'] == init_art_wiki['modified']
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created'] + 1
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified']
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created'] + 1
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified']
+        
+        #In that case, last month stats should not be changed
+        new_date = datetime.utcnow() + timedelta(-32)
+        self.user.stats.addNewArtifact('Wiki', new_date, p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+
+        assert lm_art['created'] == init_lm_art['created'] + 1
+        assert lm_art['modified'] == init_lm_art['modified']
+        assert artifacts['created'] == init_art['created'] + 2
+        assert artifacts['modified'] == init_art['modified']
+        assert art_wiki['created'] == init_art_wiki['created'] + 2
+        assert art_wiki['modified'] == init_art_wiki['modified']
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created'] + 2
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified']
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created'] + 1
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified']
+        
+        p.trove_topic = [topic._id]
+
+        self.user.stats.addNewArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+        art_sci = self.user.stats.getArtifacts(category=topic._id)
+        art_by_cat = self.user.stats.getArtifactsByCategory(detailed=True)
+
+        assert lm_art['created'] == init_lm_art['created'] + 2
+        assert lm_art['modified'] == init_lm_art['modified']
+        assert artifacts['created'] == init_art['created'] + 3
+        assert artifacts['modified'] == init_art['modified']
+        assert art_wiki['created'] == init_art_wiki['created'] + 3
+        assert art_wiki['modified'] == init_art_wiki['modified']
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created'] + 3
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified']
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created'] + 2
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified']
+        assert art_sci['created'] == init_art_sci['created'] + 1
+        assert art_sci['modified'] == init_art_sci['modified']
+        assert dict(messagetype='Wiki', created= 1, modified = 0) in art_by_cat[topic]
+        art_by_cat = self.user.stats.getArtifactsByCategory(detailed=False)
+        assert art_by_cat[topic]['created'] == 1 and art_by_cat[topic]['modified'] == 0
+
+    @td.with_user_project('test-new-user')
+    def test_modify_artifact_stats(self):
+        p = Project.query.get(shortname='u/test-new-user')
+        topic = TroveCategory.query.get(shortname='scientific')
+
+        init_lm_art = self.user.stats.getLastMonthArtifacts()
+        init_art = self.user.stats.getArtifacts()
+        init_art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        init_art_by_type = self.user.stats.getArtifactsByType()
+        init_lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+        init_art_sci = self.user.stats.getArtifacts(category=topic._id)
+
+        self.user.stats.addModifiedArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+
+        assert lm_art['created'] == init_lm_art['created']
+        assert lm_art['modified'] == init_lm_art['modified'] + 1
+        assert artifacts['created'] == init_art['created']
+        assert artifacts['modified'] == init_art['modified'] + 1
+        assert art_wiki['created'] == init_art_wiki['created']
+        assert art_wiki['modified'] == init_art_wiki['modified'] + 1
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created']
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified'] + 1
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created']
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified'] + 1
+        
+        #In that case, last month stats should not be changed
+        new_date = datetime.utcnow() + timedelta(-32)
+        self.user.stats.addModifiedArtifact('Wiki', new_date, p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+
+        assert lm_art['created'] == init_lm_art['created'] 
+        assert lm_art['modified'] == init_lm_art['modified'] + 1
+        assert artifacts['created'] == init_art['created'] 
+        assert artifacts['modified'] == init_art['modified'] + 2
+        assert art_wiki['created'] == init_art_wiki['created']
+        assert art_wiki['modified'] == init_art_wiki['modified'] + 2
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created']
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified'] + 2
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created']
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified'] + 1
+        
+        p.trove_topic = [topic._id]
+
+        self.user.stats.addModifiedArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.user.stats.getLastMonthArtifacts()
+        artifacts = self.user.stats.getArtifacts()
+        art_wiki = self.user.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.user.stats.getArtifactsByType()
+        lm_art_by_type = self.user.stats.getLastMonthArtifactsByType()
+        art_sci = self.user.stats.getArtifacts(category=topic._id)
+        art_by_cat = self.user.stats.getArtifactsByCategory(detailed=True)
+
+        assert lm_art['created'] == init_lm_art['created'] 
+        assert lm_art['modified'] == init_lm_art['modified'] + 2
+        assert artifacts['created'] == init_art['created']
+        assert artifacts['modified'] == init_art['modified'] + 3
+        assert art_wiki['created'] == init_art_wiki['created']
+        assert art_wiki['modified'] == init_art_wiki['modified'] + 3
+        assert art_by_type['Wiki']['created'] == init_art_by_type['Wiki']['created'] 
+        assert art_by_type['Wiki']['modified'] == init_art_by_type['Wiki']['modified'] + 3
+        assert lm_art_by_type['Wiki']['created'] == init_lm_art_by_type['Wiki']['created']
+        assert lm_art_by_type['Wiki']['modified'] == init_lm_art_by_type['Wiki']['modified'] +2
+        assert art_sci['created'] == init_art_sci['created']
+        assert art_sci['modified'] == init_art_sci['modified'] + 1
+        assert dict(messagetype='Wiki', created=0, modified=1) in art_by_cat[topic]
+        art_by_cat = self.user.stats.getArtifactsByCategory(detailed=False)
+        assert art_by_cat[topic]['created'] == 0 and art_by_cat[topic]['modified'] == 1
+
+    @td.with_user_project('test-new-user')
+    def test_ticket_stats(self):
+        p = Project.query.get(shortname='u/test-new-user')
+        topic = TroveCategory.query.get(shortname='scientific')
+        create_time = datetime.utcnow() + timedelta(-5)
+
+        init_lm_tickets_art = self.user.stats.getLastMonthArtifacts(art_type='Ticket')
+        init_tickets_art = self.user.stats.getArtifacts(art_type='Ticket')
+        init_tickets_sci_art = self.user.stats.getArtifacts(category=topic._id)
+        init_tickets = self.user.stats.getTickets()
+        init_lm_tickets = self.user.stats.getLastMonthTickets()
+
+        self.user.stats.addNewArtifact('Ticket', create_time, p)
+        lm_tickets_art = self.user.stats.getLastMonthArtifacts(art_type='Ticket')
+        tickets_art = self.user.stats.getArtifacts(art_type='Ticket')
+        tickets_sci_art = self.user.stats.getArtifacts(category=topic._id)
+
+        assert lm_tickets_art['created'] == init_lm_tickets_art['created'] + 1
+        assert lm_tickets_art['modified'] == init_lm_tickets_art['modified']
+        assert tickets_art['created'] == init_tickets_art['created'] + 1
+        assert tickets_art['modified'] == init_tickets_art['modified']
+        assert tickets_sci_art['created'] == tickets_sci_art['created']
+        assert tickets_sci_art['modified'] == tickets_sci_art['modified']
+        
+        p.trove_topic = [topic._id]
+
+        self.user.stats.addAssignedTicket(create_time, p)
+        tickets = self.user.stats.getTickets()
+        lm_tickets = self.user.stats.getLastMonthTickets()
+
+        assert tickets['assigned'] == init_tickets['assigned'] + 1 
+        assert tickets['revoked'] == init_tickets['revoked']
+        assert tickets['solved'] == init_tickets['solved'] 
+        assert tickets['averagesolvingtime'] is None 
+        assert lm_tickets['assigned'] == init_lm_tickets['assigned'] + 1 
+        assert lm_tickets['revoked'] == init_lm_tickets['revoked']
+        assert lm_tickets['solved'] == init_lm_tickets['solved'] 
+        assert lm_tickets['averagesolvingtime'] is None 
+
+        self.user.stats.addRevokedTicket(create_time + timedelta(-32), p)
+        tickets = self.user.stats.getTickets()
+
+        assert tickets['assigned'] == init_tickets['assigned'] + 1 
+        assert tickets['revoked'] == init_tickets['revoked'] + 1
+        assert tickets['solved'] == init_tickets['solved'] 
+        assert tickets['averagesolvingtime'] is None 
+        assert lm_tickets['assigned'] == init_lm_tickets['assigned'] + 1 
+        assert lm_tickets['revoked'] == init_lm_tickets['revoked']
+        assert lm_tickets['solved'] == init_lm_tickets['solved'] 
+        assert lm_tickets['averagesolvingtime'] is None 
+
+        self.user.stats.addClosedTicket(create_time, create_time + timedelta(1), p)
+        tickets = self.user.stats.getTickets()
+        lm_tickets = self.user.stats.getLastMonthTickets()
+
+        assert tickets['assigned'] == init_tickets['assigned'] + 1 
+        assert tickets['revoked'] == init_tickets['revoked'] + 1
+        assert tickets['solved'] == init_tickets['solved'] + 1
+
+        solving_time = dict(seconds=0,minutes=0,days=1,hours=0)
+        assert tickets['averagesolvingtime'] == solving_time
+        assert lm_tickets['assigned'] == init_lm_tickets['assigned'] + 1
+        assert lm_tickets['revoked'] == init_lm_tickets['revoked']
+        assert lm_tickets['solved'] == init_lm_tickets['solved'] + 1
+        assert lm_tickets['averagesolvingtime'] == solving_time
+
+        p.trove_topic = []
+        self.user.stats.addClosedTicket(create_time, create_time + timedelta(3), p)
+        tickets = self.user.stats.getTickets()
+        lm_tickets = self.user.stats.getLastMonthTickets()
+
+        solving_time = dict(seconds=0,minutes=0,days=2,hours=0)
+
+        assert tickets['assigned'] == init_tickets['assigned'] + 1 
+        assert tickets['revoked'] == init_tickets['revoked'] + 1
+        assert tickets['solved'] == init_tickets['solved'] + 2
+        assert tickets['averagesolvingtime'] == solving_time
+        assert lm_tickets['assigned'] == init_lm_tickets['assigned'] + 1
+        assert lm_tickets['revoked'] == init_lm_tickets['revoked']
+        assert lm_tickets['solved'] == init_lm_tickets['solved'] + 2
+        assert lm_tickets['averagesolvingtime'] == solving_time
+
+        by_cat = self.user.stats.getTicketsByCategory()
+        lm_by_cat = self.user.stats.getLastMonthTicketsByCategory()
+        solving_time=dict(days=1,hours=0,minutes=0,seconds=0)
+
+        assert by_cat[topic]['assigned'] == 1 
+        assert by_cat[topic]['revoked'] == 1
+        assert by_cat[topic]['solved'] == 1
+        assert by_cat[topic]['averagesolvingtime'] == solving_time
+        assert lm_by_cat[topic]['assigned'] == 1
+        assert lm_by_cat[topic]['revoked'] == 0
+        assert lm_by_cat[topic]['solved'] == 1
+        assert lm_by_cat[topic]['averagesolvingtime'] == solving_time
+
+    @with_git
+    @td.with_user_project('test-new-user')
+    def test_commit_stats(self):
+        p = Project.query.get(shortname='u/test-new-user')
+        topic = TroveCategory.query.get(shortname='scientific')
+        commit_time = datetime.utcnow() + timedelta(-1)
+
+        self.user.set_password('testpassword')
+        addr = M.EmailAddress.upsert('rcopeland@geek.net')
+        self.user.claim_address('rcopeland@geek.net')
+        
+        repo_dir = pkg_resources.resource_filename(
+            'forgeuserstats', 'tests/data')
+
+        c.app.repo.fs_path = repo_dir
+        c.app.repo.name = 'testgit.git'
+        repo = c.app.repo
+        repo.refresh()
+        commit = M.repo.Commit.query.get(_id=repo.heads[0]['object_id'])
+        commit.repo = repo
+
+        init_commits = self.user.stats.getCommits()
+        assert init_commits['number'] == 4
+        init_lmcommits = self.user.stats.getLastMonthCommits()
+        assert init_lmcommits['number'] == 4
+ 
+        p.trove_topic = [topic._id]
+        self.user.stats.addCommit(commit, datetime.utcnow(), p)
+        commits = self.user.stats.getCommits()
+        assert commits['number'] == init_commits['number'] + 1
+        assert commits['lines'] == init_commits['lines'] + 1
+        lmcommits = self.user.stats.getLastMonthCommits()
+        assert lmcommits['number'] == init_lmcommits['number'] + 1
+        assert lmcommits['lines'] == init_lmcommits['lines'] + 1
+        by_cat = self.user.stats.getCommitsByCategory()
+        assert by_cat[topic]['number'] == 1
+        assert by_cat[topic]['lines'] == 1
+        lm_by_cat = self.user.stats.getLastMonthCommitsByCategory()
+        assert lm_by_cat[topic]['number'] == 1
+        assert lm_by_cat[topic]['lines'] == 1
+
+        self.user.stats.addCommit(commit, datetime.utcnow() + timedelta(-40), p)
+        commits = self.user.stats.getCommits()
+        assert commits['number'] == init_commits['number'] + 2
+        assert commits['lines'] == init_commits['lines'] + 2
+        lmcommits = self.user.stats.getLastMonthCommits()
+        assert lmcommits['number'] == init_lmcommits['number'] + 1
+        assert lmcommits['lines'] == init_lmcommits['lines'] + 1
+        by_cat = self.user.stats.getCommitsByCategory()
+        assert by_cat[topic]['number'] == 2
+        assert by_cat[topic]['lines'] == 2
+        lm_by_cat = self.user.stats.getLastMonthCommitsByCategory()
+        assert lm_by_cat[topic]['number'] == 1
+        assert lm_by_cat[topic]['lines'] == 1
+
+    @td.with_user_project('test-new-user')
+    def test_login_stats(self):
+        init_logins = self.user.stats.tot_logins_count
+        init_lm_logins = self.user.stats.getLastMonthLogins()
+        
+        login_datetime = datetime.utcnow()
+        self.user.stats.addLogin(login_datetime)
+        logins = self.user.stats.tot_logins_count
+        lm_logins = self.user.stats.getLastMonthLogins()
+        assert logins == init_logins + 1 
+        assert lm_logins == init_lm_logins + 1 
+        assert abs(self.user.stats.last_login - login_datetime) < timedelta(seconds=1)
+
+        self.user.stats.addLogin(datetime.utcnow() + timedelta(-32))
+        logins = self.user.stats.tot_logins_count
+        lm_logins = self.user.stats.getLastMonthLogins()
+        assert logins == init_logins + 2
+        assert lm_logins == init_lm_logins + 1 
+        assert abs(self.user.stats.last_login - login_datetime) < timedelta(seconds=1)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/eea9aa68/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
index a3e3482..e4954b7 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_stats.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -16,38 +16,6 @@ from forgetracker import model as TM
 
 class TestStats(TestController):
 
-    @td.with_user_project('test-user')
-    def test_init_values(self):
-        user = User.register(dict(username='test-new-user',
-            display_name='Test Stats'),
-            make_project=False)
-
-        artifacts = user.stats.getArtifacts()
-        tickets = user.stats.getTickets()
-        commits = user.stats.getCommits()
-        assert user.stats.tot_logins_count == 0
-        assert artifacts['created'] == 0
-        assert artifacts['modified'] == 0
-        assert tickets['assigned'] == 0
-        assert tickets['solved'] == 0
-        assert tickets['revoked'] == 0
-        assert tickets['averagesolvingtime'] is None
-        assert commits['number'] == 0
-        assert commits['lines'] == 0
-
-        lmartifacts = user.stats.getLastMonthArtifacts()
-        lmtickets = user.stats.getLastMonthTickets()
-        lmcommits = user.stats.getLastMonthCommits()
-        assert user.stats.getLastMonthLogins() == 0
-        assert lmartifacts['created'] == 0
-        assert lmartifacts['modified'] == 0
-        assert lmtickets['assigned'] == 0
-        assert lmtickets['solved'] == 0
-        assert lmtickets['revoked'] == 0
-        assert lmtickets['averagesolvingtime'] is None
-        assert lmcommits['number'] == 0
-        assert lmcommits['lines'] == 0
-
     def test_login(self):
         user = User.by_username('test-user')
         init_logins = c.user.stats.tot_logins_count
@@ -198,4 +166,3 @@ class TestGitCommit(unittest.TestCase, TestController):
         assert commits['number'] == 4
         lmcommits = c.user.stats.getLastMonthCommits()
         assert lmcommits['number'] == 4
-


[16/48] git commit: [5453] fixed bug

Posted by tv...@apache.org.
[5453] fixed bug


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

Branch: refs/heads/si/5453
Commit: 6c9b889b653fc437a26c6810ec2a21caaf635772
Parents: 35f76dd
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 10 19:25:44 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6c9b889b/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index c622888..d196709 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -185,7 +185,7 @@ class UserStats(MappedClass):
         j = getElementIndex(cat.messages, art_type = art_type)
         if j is None:
             return dict(created=0, modified=0)
-        return dict(created=cat[j].created, modified=cat[j].modified)
+        return dict(created=cat.messages[j].created, modified=cat.messages[j].modified)
 
     def getTickets(self, category = None):
         i = getElementIndex(self.general, category = category)


[20/48] git commit: [5453] Logins count

Posted by tv...@apache.org.
[5453] Logins count


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

Branch: refs/heads/si/5453
Commit: 0df516ef8fc9c8be3ce8164d5f1473d46bf8e8e4
Parents: a326694
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 9 09:19:56 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:17 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/plugin.py |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0df516ef/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 3e33172..e10c0d4 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -98,6 +98,8 @@ class AuthenticationProvider(object):
             self.session['userid'] = user._id
             self.session.save()
             g.zarkov_event('login', user=user)
+            for l in g.statslisteners:
+                l.addUserLogin(user)
             return user
         except exc.HTTPUnauthorized:
             self.logout()


[05/48] git commit: [#5909] Split Contacts and Availability to separate pages

Posted by tv...@apache.org.
[#5909] Split Contacts and Availability to separate pages


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

Branch: refs/heads/si/5453
Commit: 8feb54191dbfb25b879a608249c54daba20abcda
Parents: 0e7d7f6
Author: Simone Gatti <si...@gmail.com>
Authored: Wed Mar 13 04:35:43 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:46 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py              |  214 ++++++++++---------
 Allura/allura/templates/user_availability.html |   74 +++++++
 Allura/allura/templates/user_contacts.html     |   55 +++++
 Allura/allura/templates/user_preferences.html  |  100 +--------
 Allura/allura/tests/functional/test_auth.py    |   44 ++--
 5 files changed, 274 insertions(+), 213 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8feb5419/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 8b0be12..462f449 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -366,9 +366,124 @@ class UserSkillsController(BaseController):
         flash('Your skills list was successfully updated!')
         redirect('/auth/prefs/user_skills')
 
+class UserContactsController(BaseController):
+
+    @expose('jinja:allura:templates/user_contacts.html')
+    def index(self, **kw):
+        require_authenticated()
+        return dict()
+
+    @expose()
+    @require_post()
+    @validate(F.add_socialnetwork_form, error_handler=index)
+    def add_social_network(self, **kw):
+        require_authenticated()
+        c.user.add_socialnetwork(kw['socialnetwork'], kw['accounturl'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_socialnetwork_form, error_handler=index)
+    def remove_social_network(self, **kw):
+        require_authenticated()
+        c.user.remove_socialnetwork(kw['socialnetwork'], kw['account'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.add_telnumber_form, error_handler=index)
+    def add_telnumber(self, **kw):
+        require_authenticated()
+        c.user.add_telephonenumber(kw['newnumber'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_textvalue_form, error_handler=index)
+    def remove_telnumber(self, **kw):
+        require_authenticated()
+        c.user.remove_telephonenumber(kw['oldvalue'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.add_website_form, error_handler=index)
+    def add_webpage(self, **kw):
+        require_authenticated()
+        c.user.add_webpage(kw['newwebsite'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_textvalue_form, error_handler=index)
+    def remove_webpage(self, **kw):
+        require_authenticated()
+        c.user.remove_webpage(kw['oldvalue'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.skype_account_form, error_handler=index)
+    def skype_account(self, **kw):
+        require_authenticated()
+        c.user.set_pref('skypeaccount', kw['skypeaccount'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.')
+
+class UserAvailabilityController(BaseController):
+
+    @expose('jinja:allura:templates/user_availability.html')
+    def index(self, **kw):
+        require_authenticated()
+        return dict()
+
+    @expose()
+    @require_post()
+    @validate(F.add_timeslot_form, error_handler=index)
+    def add_timeslot(self, **kw):
+        require_authenticated()
+        c.user.add_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_timeslot_form, error_handler=index)
+    def remove_timeslot(self, **kw):
+        require_authenticated()
+        c.user.remove_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.add_inactive_period_form, error_handler=index)
+    def add_inactive_period(self, **kw):
+        require_authenticated()
+        c.user.add_inactive_period(kw['startdate'], kw['enddate'])
+        flash('Your inactivity periods were successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_inactive_period_form, error_handler=index)
+    def remove_inactive_period(self, **kw):
+        require_authenticated()
+        c.user.remove_inactive_period(kw['startdate'], kw['enddate'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.')
+
 class PreferencesController(BaseController):
 
     user_skills = UserSkillsController()
+    user_contacts = UserContactsController()
+    user_availability = UserAvailabilityController()
 
     @with_trailing_slash
     @expose('jinja:allura:templates/user_preferences.html')
@@ -558,105 +673,6 @@ class PreferencesController(BaseController):
 
     @expose()
     @require_post()
-    @validate(F.add_socialnetwork_form, error_handler=index)
-    def add_social_network(self, **kw):
-        require_authenticated()
-        c.user.add_socialnetwork(kw['socialnetwork'], kw['accounturl'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.remove_socialnetwork_form, error_handler=index)
-    def remove_social_network(self, **kw):
-        require_authenticated()
-        c.user.remove_socialnetwork(kw['socialnetwork'], kw['account'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.add_telnumber_form, error_handler=index)
-    def add_telnumber(self, **kw):
-        require_authenticated()
-        c.user.add_telephonenumber(kw['newnumber'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.remove_textvalue_form, error_handler=index)
-    def remove_telnumber(self, **kw):
-        require_authenticated()
-        c.user.remove_telephonenumber(kw['oldvalue'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.add_website_form, error_handler=index)
-    def add_webpage(self, **kw):
-        require_authenticated()
-        c.user.add_webpage(kw['newwebsite'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.remove_textvalue_form, error_handler=index)
-    def remove_webpage(self, **kw):
-        require_authenticated()
-        c.user.remove_webpage(kw['oldvalue'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.skype_account_form, error_handler=index)
-    def skype_account(self, **kw):
-        require_authenticated()
-        c.user.set_pref('skypeaccount', kw['skypeaccount'])
-        flash('Your personal contacts were successfully updated!')
-        redirect('.#Contacts')
-
-    @expose()
-    @require_post()
-    @validate(F.add_timeslot_form, error_handler=index)
-    def add_timeslot(self, **kw):
-        require_authenticated()
-        c.user.add_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
-        flash('Your availability timeslots were successfully updated!')
-        redirect('.#Availability')
-
-    @expose()
-    @require_post()
-    @validate(F.remove_timeslot_form, error_handler=index)
-    def remove_timeslot(self, **kw):
-        require_authenticated()
-        c.user.remove_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
-        flash('Your availability timeslots were successfully updated!')
-        redirect('.#Availability')
-
-    @expose()
-    @require_post()
-    @validate(F.add_inactive_period_form, error_handler=index)
-    def add_inactive_period(self, **kw):
-        require_authenticated()
-        c.user.add_inactive_period(kw['startdate'], kw['enddate'])
-        flash('Your inactivity periods were successfully updated!')
-        redirect('.#Availability')
-
-    @expose()
-    @require_post()
-    @validate(F.remove_inactive_period_form, error_handler=index)
-    def remove_inactive_period(self, **kw):
-        require_authenticated()
-        c.user.remove_inactive_period(kw['startdate'], kw['enddate'])
-        flash('Your availability timeslots were successfully updated!')
-        redirect('.#Availability')
-
-    @expose()
-    @require_post()
     def upload_sshkey(self, key=None):
         ap = plugin.AuthenticationProvider.get(request)
         try:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8feb5419/Allura/allura/templates/user_availability.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_availability.html b/Allura/allura/templates/user_availability.html
new file mode 100644
index 0000000..d7f46b7
--- /dev/null
+++ b/Allura/allura/templates/user_availability.html
@@ -0,0 +1,74 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Availability{% endblock %}
+
+{% block header %}Availability timeslots of {{c.user.username}} {% endblock %}
+
+{% block content %}
+  <div class="grid-20">
+    <h2>Availability</h2>
+    <div class="grid-18">
+      Please, set your time intervals choosing a weekday and entering the time interval according to the timezone specified in your personal data, using the format HH:MM. If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored.
+    </div>
+    <div class="grid-18">
+      You can also specify periods of time during which you won't be able to work on the forge, in orther to communicate other users
+      that they can't contact you during those days. Please, do it specifying date intervals in format DD/MM/YYYY.
+    </div> 
+  </div>
+  <div class="grid-20">
+    {%if c.user.get_availability_timeslots() %}
+      <h3>Existing availability timeslots</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Weekday</th>
+            <th>Start time</th>
+            <th>End time</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ts in c.user.get_availability_timeslots() %}
+          {{g.theme.remove_timeslot_form.display(
+                action="/auth/prefs/user_availability/remove_timeslot",
+                weekday=ts.week_day,
+                starttime=ts.start_time,
+                endtime=ts.end_time)}} 
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new availability timeslot</h3>
+    {{g.theme.add_timeslot_form.display(action="/auth/prefs/user_availability/add_timeslot")}}
+  </div>
+
+  <div class="grid-20">
+    {%if c.user.get_inactive_periods() %}
+      <h3>Existing periods of inactivity on the forge</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Start date</th>
+            <th>End date</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ip in c.user.get_inactive_periods() %}
+          {{g.theme.remove_inactive_period_form.display(
+                action="/auth/prefs/user_availability/remove_inactive_period",
+                startdate=ip.start_date,
+                enddate=ip.end_date)}} 
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new period of inactivity on the forge</h3>
+    {{g.theme.add_inactive_period_form.display(action="/auth/prefs/user_availability/add_inactive_period")}}
+    <h3>Other possible actions</h3>
+    <div class="grid-20" style="margin-bottom:10px;"/>
+      <ul>
+        <li>
+          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
+        </li>
+      </ul>
+    </div>
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8feb5419/Allura/allura/templates/user_contacts.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_contacts.html b/Allura/allura/templates/user_contacts.html
new file mode 100644
index 0000000..d6d1811
--- /dev/null
+++ b/Allura/allura/templates/user_contacts.html
@@ -0,0 +1,55 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Contacts{% endblock %}
+
+{% block header %}Contacts of {{c.user.username}} {% endblock %}
+
+{% block content %}
+  <div class="grid-20">
+    <h2>Personal Contacts</h2>
+    <h3>Skype account</h3>
+
+    {{g.theme.skype_account_form.display(action="/auth/prefs/user_contacts/skype_account",
+          initial_value=c.user.get_pref('skypeaccount'))}}
+     
+    {%if c.user.get_pref('socialnetworks') or c.user.get_pref('telnumbers') or c.user.get_pref('webpages') %}
+      <h3>Other existing contacts</h3>
+        <table>
+          <tr>
+            <thead>
+              <th>Type</th>
+              <th>Contact</th>
+              <th>Actions</th>
+            </thead>
+          </tr>
+          {% for sn in c.user.get_pref('socialnetworks') %}
+             {{g.theme.remove_socialnetwork_form.display(action="/auth/prefs/user_contacts/remove_social_network", account=sn.accounturl, socialnetwork=sn.socialnetwork)}} 
+          {% endfor %}
+
+          {% for tn in c.user.get_pref('telnumbers') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/user_contacts/remove_telnumber", value=tn, label="Telephone number")}} 
+          {%endfor%}
+
+          {% for ws in c.user.get_pref('webpages') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/user_contacts/remove_webpage", value=ws, label="Website url")}} 
+          {%endfor%}
+        </table>
+    {% endif %}
+
+    <h3>Add a social network account</h3>
+    {{g.theme.add_socialnetwork_form.display(action="/auth/prefs/user_contacts/add_social_network")}}
+    <h3>Add a telephone number</h3>
+    {{g.theme.add_telnumber_form.display(action="/auth/prefs/user_contacts/add_telnumber")}}
+    <h3>Add a personal website</h3>
+    {{g.theme.add_website_form.display(action="/auth/prefs/user_contacts/add_webpage")}}
+  <h3>Other possible actions</h3>
+    <div class="grid-20" style="margin-bottom:10px;"/>
+      <ul>
+        <li>
+          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
+        </li>
+      </ul>
+    </div>
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8feb5419/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
index be4dfe6..4e1391e 100644
--- a/Allura/allura/templates/user_preferences.html
+++ b/Allura/allura/templates/user_preferences.html
@@ -25,103 +25,19 @@
   <div style="clear:both" class="grid-20">
     <a name="Contacts"></a>
     <h2>Personal Contacts</h2>
-    <h3>Skype account</h3>
-
-    {{g.theme.skype_account_form.display(action="/auth/prefs/skype_account",
-          initial_value=c.user.get_pref('skypeaccount'))}}
-     
-    {%if c.user.get_pref('socialnetworks') or c.user.get_pref('telnumbers') or c.user.get_pref('webpages') %}
-      <h3>Other existing contacts</h3>
-        <table>
-          <tr>
-            <thead>
-              <th>Type</th>
-              <th>Contact</th>
-              <th>Actions</th>
-            </thead>
-          </tr>
-          {% for sn in c.user.get_pref('socialnetworks') %}
-             {{g.theme.remove_socialnetwork_form.display(account=sn.accounturl, socialnetwork=sn.socialnetwork)}} 
-          {% endfor %}
-
-          {% for tn in c.user.get_pref('telnumbers') %}
-              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_telnumber", value=tn, label="Telephone number")}} 
-          {%endfor%}
-
-          {% for ws in c.user.get_pref('webpages') %}
-              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_webpage", value=ws, label="Website url")}} 
-          {%endfor%}
-        </table>
-    {% endif %}
-
-    <h3>Add a social network account</h3>
-    {{g.theme.add_socialnetwork_form.display(action="/auth/prefs/add_social_network")}}
-    <h3>Add a telephone number</h3>
-    {{g.theme.add_telnumber_form.display(action="/auth/prefs/add_telnumber")}}
-    <h3>Add a personal website</h3>
-    {{g.theme.add_website_form.display(action="/auth/prefs/add_webpage")}}
+    <p>
+      If you want, you can set your contacts allowing other members to contact you or you can set your personal website allowing other members to introduce your self.
+    </p>
+    <ul><li><a href="/auth/prefs/user_contacts">Click here to check and change your contacts</a></li></ul>
   </div>
 
   <a name="Availability"></a>
   <div style="clear:both" class="grid-20">
     <h2>Availability</h2>
-    <div class="grid-18">
-      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge.
-      Please, set your time intervals choosing a weekday and entering the time interval according to the timezone specified in your 
-      personal data, using the format HH:MM. If you didn't set any timezone, your timeslots could be meaningless to other users, 
-      therefore they will be ignored.
-    </div>
-    <div class="grid-18">
-      You can also specify periods of time during which you won't be able to work on the forge, in orther to communicate other users
-      that they can't contact you during those days. Please, do it specifying date intervals in format DD/MM/YYYY.
-    </div> 
-  </div>
-  <div class="grid-20">
-    {%if c.user.get_availability_timeslots() %}
-      <h3>Existing availability timeslots</h3>
-      <table>
-        <tr>
-          <thead>
-            <th>Weekday</th>
-            <th>Start time</th>
-            <th>End time</th>
-            <th>Actions</th>
-          </thead>
-        </tr>
-        {% for ts in c.user.get_availability_timeslots() %}
-          {{g.theme.remove_timeslot_form.display(
-                action="/auth/prefs/remove_timeslot",
-                weekday=ts.week_day,
-                starttime=ts.start_time,
-                endtime=ts.end_time)}} 
-        {%endfor%}
-      </table>
-    {% endif %}
-    <h3>Add a new availability timeslot</h3>
-    {{g.theme.add_timeslot_form.display(action="/auth/prefs/add_timeslot")}}
-  </div>
-
-  <div class="grid-20">
-    {%if c.user.get_inactive_periods() %}
-      <h3>Existing periods of inactivity on the forge</h3>
-      <table>
-        <tr>
-          <thead>
-            <th>Start date</th>
-            <th>End date</th>
-            <th>Actions</th>
-          </thead>
-        </tr>
-        {% for ip in c.user.get_inactive_periods() %}
-          {{g.theme.remove_inactive_period_form.display(
-                action="/auth/prefs/remove_inactive_period",
-                startdate=ip.start_date,
-                enddate=ip.end_date)}} 
-        {%endfor%}
-      </table>
-    {% endif %}
-    <h3>Add a new period of inactivity on the forge</h3>
-    {{g.theme.add_inactive_period_form.display(action="/auth/prefs/add_inactive_period")}}
+    <p>
+      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge. (If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored!)
+    </p>
+    <ul><li><a href="/auth/prefs/user_availability">Click here to check and change your availability slots</a></li></ul>
   </div>
 
   <div class="grid-20">

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8feb5419/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 d7b5f89..0b4bb1c 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -435,8 +435,8 @@ class TestPreferences(TestController):
     def test_contacts(self):
         #Add skype account
         testvalue = 'testaccount'
-        result = self.app.get('/auth/prefs')
-        r = self.app.post('/auth/prefs/skype_account',
+        result = self.app.get('/auth/prefs/user_contacts')
+        r = self.app.post('/auth/prefs/user_contacts/skype_account',
              params=dict(skypeaccount=testvalue))
         user = M.User.query.get(username='test-admin')
         assert user.skypeaccount == testvalue
@@ -444,7 +444,7 @@ class TestPreferences(TestController):
         #Add social network account
         socialnetwork = 'Facebook'
         accounturl = 'http://www.facebook.com/test'
-        r = self.app.post('/auth/prefs/add_social_network',
+        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork,
                          accounturl = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -455,7 +455,7 @@ class TestPreferences(TestController):
         #Add second social network account
         socialnetwork2 = 'Twitter'
         accounturl2 = 'http://twitter.com/test'
-        r = self.app.post('/auth/prefs/add_social_network',
+        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork2,
                          accounturl = '@test'))
         user = M.User.query.get(username='test-admin')
@@ -464,7 +464,7 @@ class TestPreferences(TestController):
                 {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks)
 
         #Remove first social network account
-        r = self.app.post('/auth/prefs/remove_social_network',
+        r = self.app.post('/auth/prefs/user_contacts/remove_social_network',
              params=dict(socialnetwork=socialnetwork,
                          account = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -472,7 +472,7 @@ class TestPreferences(TestController):
                {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks
 
         #Add invalid social network account
-        r = self.app.post('/auth/prefs/add_social_network',
+        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
              params=dict(accounturl = accounturl, socialnetwork=''))
         user = M.User.query.get(username='test-admin')
         assert len(user.socialnetworks) == 1 and \
@@ -480,40 +480,40 @@ class TestPreferences(TestController):
 
         #Add telephone number
         telnumber = '+3902123456'
-        r = self.app.post('/auth/prefs/add_telnumber',
+        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
              params=dict(newnumber=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and (user.telnumbers[0] == telnumber))
 
         #Add second telephone number
         telnumber2 = '+3902654321'
-        r = self.app.post('/auth/prefs/add_telnumber',
+        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
              params=dict(newnumber=telnumber2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 2 and telnumber in user.telnumbers and telnumber2 in user.telnumbers)
 
         #Remove first telephone number
-        r = self.app.post('/auth/prefs/remove_telnumber',
+        r = self.app.post('/auth/prefs/user_contacts/remove_telnumber',
              params=dict(oldvalue=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and telnumber2 in user.telnumbers)
 
         #Add website
         website = 'http://www.testurl.com'
-        r = self.app.post('/auth/prefs/add_webpage',
+        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
              params=dict(newwebsite=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and (website in user.webpages))
 
         #Add second website
         website2 = 'http://www.testurl2.com'
-        r = self.app.post('/auth/prefs/add_webpage',
+        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
              params=dict(newwebsite=website2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 2 and website in user.webpages and website2 in user.webpages)
 
         #Remove first website
-        r = self.app.post('/auth/prefs/remove_webpage',
+        r = self.app.post('/auth/prefs/user_contacts/remove_webpage',
              params=dict(oldvalue=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and website2 in user.webpages)
@@ -527,8 +527,8 @@ class TestPreferences(TestController):
         starttime = time(9,0,0)
         endtime = time(12, 0, 0)
 
-        result = self.app.get('/auth/prefs')
-        r = self.app.post('/auth/prefs/add_timeslot',
+        result = self.app.get('/auth/prefs/user_availability')
+        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -542,7 +542,7 @@ class TestPreferences(TestController):
         endtime2 = time(16, 0, 0)
 
         #Add second availability timeslot
-        r = self.app.post('/auth/prefs/add_timeslot',
+        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=starttime2.strftime('%H:%M'),
@@ -553,7 +553,7 @@ class TestPreferences(TestController):
                and timeslot2dict in user.get_availability_timeslots()
 
         #Remove availability timeslot
-        r = self.app.post('/auth/prefs/remove_timeslot',
+        r = self.app.post('/auth/prefs/user_availability/remove_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -562,7 +562,7 @@ class TestPreferences(TestController):
         assert len(user.availability) == 1 and timeslot2dict in user.get_availability_timeslots()
 
         #Add invalid availability timeslot
-        r = self.app.post('/auth/prefs/add_timeslot',
+        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=endtime2.strftime('%H:%M'),
@@ -581,8 +581,8 @@ class TestPreferences(TestController):
         now = datetime(now.year, now.month, now.day)
         startdate = now + timedelta(days=1)
         enddate = now + timedelta(days=7)
-        result = self.app.get('/auth/prefs')
-        r = self.app.post('/auth/prefs/add_inactive_period',
+        result = self.app.get('/auth/prefs/user_availability')
+        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -593,7 +593,7 @@ class TestPreferences(TestController):
         #Add second inactivity period
         startdate2 =  now + timedelta(days=24)
         enddate2 = now + timedelta(days=28)
-        r = self.app.post('/auth/prefs/add_inactive_period',
+        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
              params=dict(
                  startdate=startdate2.strftime('%d/%m/%Y'),
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -603,7 +603,7 @@ class TestPreferences(TestController):
                and period2dict in user.get_inactive_periods()
 
         #Remove first inactivity period
-        r = self.app.post('/auth/prefs/remove_inactive_period',
+        r = self.app.post('/auth/prefs/user_availability/remove_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -611,7 +611,7 @@ class TestPreferences(TestController):
         assert len(user.inactiveperiod) == 1 and period2dict in user.get_inactive_periods()
 
         #Add invalid inactivity period
-        r = self.app.post('/auth/prefs/add_inactive_period',
+        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
              params=dict(
                  startdate='NOT/A/DATE',
                  enddate=enddate2.strftime('%d/%m/%Y')))


[46/48] git commit: [#5453] Fixing errors in userstats

Posted by tv...@apache.org.
[#5453] Fixing errors in userstats


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

Branch: refs/heads/si/5453
Commit: 2e6455f0a39cdff5e093b55fd69d40dbec02b6fb
Parents: f0ec0be
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Mar 13 01:25:10 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/model/stats.py      |    4 ---
 ForgeUserStats/forgeuserstats/tests/test_model.py |   23 +++++++--------
 2 files changed, 11 insertions(+), 16 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2e6455f0/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 4171d52..b2f70a4 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -25,10 +25,6 @@ class UserStats(Stats):
         stats = cls(user_id=user._id,
             registration_date = datetime.utcnow())
         user.stats_id = stats._id
-        if session(stats):
-            session(stats).flush(stats)
-        if session(user):
-            session(user).flush(user)
         return stats
 
     def getLastMonthLogins(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2e6455f0/ForgeUserStats/forgeuserstats/tests/test_model.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_model.py b/ForgeUserStats/forgeuserstats/tests/test_model.py
index 3a9fcde..1f0236e 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_model.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_model.py
@@ -18,9 +18,8 @@ class TestUserStats(unittest.TestCase):
 
         setup_basic_test()
         setup_global_objects()
-        self.user = User.register(dict(username='test-new-user',
-            display_name='Test Stats'),
-            make_project=False)
+        self.user = User.by_username('test-user-2')
+        c.user = self.user
 
     def test_init_values(self):
         artifacts = self.user.stats.getArtifacts()
@@ -49,9 +48,9 @@ class TestUserStats(unittest.TestCase):
         assert lmcommits['number'] == 0
         assert lmcommits['lines'] == 0
 
-    @td.with_user_project('test-new-user')
+    @td.with_user_project('test-user-2')
     def test_create_artifact_stats(self):
-        p = Project.query.get(shortname='u/test-new-user')
+        p = Project.query.get(shortname='u/test-user-2')
         topic = TroveCategory.query.get(shortname='scientific')
 
         init_lm_art = self.user.stats.getLastMonthArtifacts()
@@ -126,9 +125,9 @@ class TestUserStats(unittest.TestCase):
         art_by_cat = self.user.stats.getArtifactsByCategory(detailed=False)
         assert art_by_cat[topic]['created'] == 1 and art_by_cat[topic]['modified'] == 0
 
-    @td.with_user_project('test-new-user')
+    @td.with_user_project('test-user-2')
     def test_modify_artifact_stats(self):
-        p = Project.query.get(shortname='u/test-new-user')
+        p = Project.query.get(shortname='u/test-user-2')
         topic = TroveCategory.query.get(shortname='scientific')
 
         init_lm_art = self.user.stats.getLastMonthArtifacts()
@@ -203,9 +202,9 @@ class TestUserStats(unittest.TestCase):
         art_by_cat = self.user.stats.getArtifactsByCategory(detailed=False)
         assert art_by_cat[topic]['created'] == 0 and art_by_cat[topic]['modified'] == 1
 
-    @td.with_user_project('test-new-user')
+    @td.with_user_project('test-user-2')
     def test_ticket_stats(self):
-        p = Project.query.get(shortname='u/test-new-user')
+        p = Project.query.get(shortname='u/test-user-2')
         topic = TroveCategory.query.get(shortname='scientific')
         create_time = datetime.utcnow() + timedelta(-5)
 
@@ -299,9 +298,9 @@ class TestUserStats(unittest.TestCase):
         assert lm_by_cat[topic]['averagesolvingtime'] == solving_time
 
     @with_git
-    @td.with_user_project('test-new-user')
+    @td.with_user_project('test-user-2')
     def test_commit_stats(self):
-        p = Project.query.get(shortname='u/test-new-user')
+        p = Project.query.get(shortname='u/test-user-2')
         topic = TroveCategory.query.get(shortname='scientific')
         commit_time = datetime.utcnow() + timedelta(-1)
 
@@ -353,7 +352,7 @@ class TestUserStats(unittest.TestCase):
         assert lm_by_cat[topic]['number'] == 1
         assert lm_by_cat[topic]['lines'] == 1
 
-    @td.with_user_project('test-new-user')
+    @td.with_user_project('test-user-2')
     def test_login_stats(self):
         init_logins = self.user.stats.tot_logins_count
         init_lm_logins = self.user.stats.getLastMonthLogins()


[27/48] git commit: [5453] Moved loop through stats listeners

Posted by tv...@apache.org.
[5453] Moved loop through stats listeners


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

Branch: refs/heads/si/5453
Commit: caf94e11d1efc126162e255e7689d3db09c2e00a
Parents: f94f0d4
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 19 17:11:42 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:16:18 2013 +0000

----------------------------------------------------------------------
 Allura/allura/eventslistener.py           |   36 ++++++++++++++++++++++++
 Allura/allura/lib/app_globals.py          |   19 +++++-------
 Allura/allura/lib/plugin.py               |    3 +-
 Allura/allura/model/artifact.py           |   10 ++----
 Allura/allura/model/auth.py               |    3 +-
 Allura/allura/model/repo_refresh.py       |    3 +-
 ForgeTracker/forgetracker/model/ticket.py |   12 +++----
 7 files changed, 56 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/eventslistener.py
----------------------------------------------------------------------
diff --git a/Allura/allura/eventslistener.py b/Allura/allura/eventslistener.py
index 15adf00..f508013 100644
--- a/Allura/allura/eventslistener.py
+++ b/Allura/allura/eventslistener.py
@@ -24,4 +24,40 @@ class EventsListener:
     def ticketEvent(self, event_type, ticket, project, user):
         pass
 
+    def addUserToOrganization(self, newMembership):
+        pass
+
+'''This class simply allows to iterate through all the registered listeners,
+so that all of them are called to update statistics.'''
+class PostEvent:
+    def __init__(self, listeners):
+        self.listeners = listeners
+
+    def __iterate(self, event, *d):
+        for l in self.listeners:
+            getattr(l, event)(*d)
+
+    def newArtifact(self, art_type, art_datetime, project, user):
+        self.__iterate('newArtifact', art_type, art_datetime, project, user)
+
+    def modifiedArtifact(self, art_type, art_datetime, project, user):
+        self.__iterate('modifiedArtifact',art_type,art_datetime,project,user)
+
+    def newUser(self, user):
+        self.__iterate('newUser', user)
+
+    def newOrganization(self, organization):
+        self.__iterate('newOrganization', organization)
+
+    def addUserLogin(self, user):
+        self.__iterate('addUserLogin', user)
+
+    def newCommit(self, newcommit, project, user):
+        self.__iterate('newCommit', newcommit, project, user)
+
+    def ticketEvent(self, event_type, ticket, project, user):
+        self.__iterate('ticketEvent', event_type, ticket, project, user)
+
+    def addUserToOrganization(self, organization):
+        self.__iterate('addUserToOrganization', organization)
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 1e9019c..71426a6 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -30,6 +30,7 @@ from ming.utils import LazyProperty
 import allura.tasks.event_tasks
 from allura import model as M
 from allura.lib.markdown_extensions import ForgeExtension
+from allura.eventslistener import PostEvent
 
 from allura.lib import gravatar, plugin, utils
 from allura.lib import helpers as h
@@ -165,23 +166,19 @@ class Globals(object):
             theme=_cache_eps('allura.theme'),
             user_prefs=_cache_eps('allura.user_prefs'),
             spam=_cache_eps('allura.spam'),
+            stats=_cache_eps('allura.stats'),
             )
 
         # Zarkov logger
         self._zarkov = None
 
-        self.show_userstats = False
+        self.show_userstats = config.get('user.stats.enable','false')=='true'
         # Set listeners to update stats
-        self.statslisteners = []
-        for ep in pkg_resources.iter_entry_points("allura.stats"):
-            if ep.name.lower() == 'userstats':
-                self.show_userstats = config.get(
-                    'user.stats.enable','false')=='true'
-                if self.show_userstats:
-                    self.statslisteners.append(ep.load()().listener)
-            else:
-                self.statslisteners.append(ep.load()().listener)
-
+        statslisteners = []
+        ep = self.entry_points['stats'].get('userstats')
+        if self.show_userstats and ep:
+            statslisteners.append(ep().listener)
+        self.statsUpdater = PostEvent(statslisteners)
 
     @LazyProperty
     def spam_checker(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index e10c0d4..2c8038f 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -98,8 +98,7 @@ class AuthenticationProvider(object):
             self.session['userid'] = user._id
             self.session.save()
             g.zarkov_event('login', user=user)
-            for l in g.statslisteners:
-                l.addUserLogin(user)
+            g.statsUpdater.addUserLogin(user)
             return user
         except exc.HTTPUnauthorized:
             self.logout()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index bce0720..6d6c162 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -375,13 +375,11 @@ class VersionedArtifact(Artifact):
                  self.version, self.__class__)
         if update_stats: 
             if self.version > 1:
-                for l in g.statslisteners:
-                    l.modifiedArtifact(
-                        self.type_s, self.mod_date, self.project, c.user)
+                g.statsUpdater.modifiedArtifact(
+                    self.type_s, self.mod_date, self.project, c.user)
             else :
-                for l in g.statslisteners:
-                    l.newArtifact(
-                        self.type_s, self.mod_date, self.project, c.user)
+                g.statsUpdater.newArtifact(
+                    self.type_s, self.mod_date, self.project, c.user)
         return ss
 
     def get_version(self, n):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index cb9bdfb..9ed7067 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -593,8 +593,7 @@ class User(MappedClass, ActivityNode, ActivityObject):
         if user and 'display_name' in doc:
             user.set_pref('display_name', doc['display_name'])
         if user:
-            for l in g.statslisteners:
-                l.newUser(user)
+            g.statsUpdater.newUser(user)
         if user and make_project:
             n = M.Neighborhood.query.get(name='Users')
             n.register_project(auth_provider.user_project_shortname(user),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 1993f57..63e2bb0 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -117,8 +117,7 @@ def refresh_repo(repo, all_commits=False, notify=True):
         if user is None:
             user = User.by_username(new.committed.name)
         if user is not None:
-            for l in g.statslisteners:
-                l.newCommit(new, repo.app_config.project, user)
+            g.statsUpdater.newCommit(new, repo.app_config.project, user)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event(

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/caf94e11/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index c2b15c7..c03662b 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -435,8 +435,7 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
                 ('Status', old.status, self.status) ]
             if old.status != self.status and self.status in c.app.globals.set_of_closed_status_names:
                 h.log_action(log, 'closed').info('')
-                for l in g.statslisteners:
-                    l.ticketEvent("closed", self, self.project, self.assigned_to)
+                g.statsUpdater.ticketEvent("closed", self, self.project, self.assigned_to)
             for key in self.custom_fields:
                 fields.append((key, old.custom_fields.get(key, ''), self.custom_fields[key]))
             for title, o, n in fields:
@@ -449,9 +448,9 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
                 changes.append('Owner updated: %r => %r' % (
                         o and o.username, n and n.username))
                 self.subscribe(user=n)
-                for l in g.statslisteners :
-                    l.ticketEvent("assigned", self, self.project, n)
-                    if o: l.ticketEvent("revoked", self, self.project, o)
+                g.statsUpdater.ticketEvent("assigned", self, self.project, n)
+                if o: 
+                    g.statsUpdater.ticketEvent("revoked", self, self.project, o)
             if old.description != self.description:
                 changes.append('Description updated:')
                 changes.append('\n'.join(
@@ -465,8 +464,7 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
             self.subscribe()
             if self.assigned_to_id:
                 user = User.query.get(_id=self.assigned_to_id)
-                for l in g.statslisteners :
-                    l.ticketEvent("assigned", self, self.project, user)
+                g.statsUpdater.ticketEvent("assigned", self, self.project, user)
                 self.subscribe(user=user)
             description = ''
             subject = self.email_subject


[38/48] git commit: [5453] Allowing to hide stats

Posted by tv...@apache.org.
[5453] Allowing to hide stats


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

Branch: refs/heads/si/5453
Commit: fa9f88ba2adb35c9b2594e4e6328742d9e6f707e
Parents: 6abbf9d
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 16 13:01:52 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:55 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py                  |   22 ++
 Allura/allura/ext/user_profile/user_main.py        |    2 +-
 Allura/allura/lib/plugin.py                        |    8 +
 Allura/allura/lib/widgets/forms.py                 |   16 +
 Allura/allura/model/contrib_stats.py               |    1 +
 Allura/allura/templates/user_preferences.html      |  287 +++++++++++++++
 .../forgeuserstats/controllers/userstats.py        |   11 +
 .../forgeuserstats/templates/artifacts.html        |   15 +-
 .../forgeuserstats/templates/commits.html          |   15 +-
 ForgeUserStats/forgeuserstats/templates/index.html |   15 +-
 .../forgeuserstats/templates/tickets.html          |   15 +-
 11 files changed, 401 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 2914799..a60b25d 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -58,6 +58,7 @@ class F(object):
     remove_inactive_period_form = forms.RemoveInactivePeriodForm()
     save_skill_form = forms.AddUserSkillForm()
     remove_skill_form = forms.RemoveSkillForm()
+    set_statistics = forms.StatsPreferencesForm()
 
 class AuthController(BaseController):
 
@@ -726,6 +727,27 @@ class SubscriptionsController(BaseController):
     @h.vardec
     @expose()
     @require_post()
+    @validate(F.set_statistics, error_handler=index)
+    def set_statistics(self, **kw):
+        require_authenticated()
+        c.user.stats.visible = kw.get('visible', True)
+        flash('Your preferences about statistics were successfully updated!')
+        redirect('.#Statistics')
+
+    @expose()
+    @require_post()
+    def upload_sshkey(self, key=None):
+        ap = plugin.AuthenticationProvider.get(request)
+        try:
+            ap.upload_sshkey(c.user.username, key)
+        except AssertionError, ae:
+            flash('Error uploading key: %s' % ae, 'error')
+        flash('Key uploaded')
+        redirect('.')
+
+    @h.vardec
+    @expose()
+    @require_post()
     @validate(F.subscription_form, error_handler=index)
     def update_subscriptions(self, subscriptions=None, **kw):
         for s in subscriptions:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/ext/user_profile/user_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/user_main.py b/Allura/allura/ext/user_profile/user_main.py
index 759f3f3..f3c3331 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -64,7 +64,7 @@ class UserProfileController(BaseController):
         user = c.project.user_project_of
         if not user:
             raise exc.HTTPNotFound()
-        if g.show_userstats:
+        if g.show_userstats and user.stats.visible:
             from forgeuserstats.main import ForgeUserStatsApp
             link, description = ForgeUserStatsApp.createlink(user)
         else:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 2c8038f..84e9f6e 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -738,6 +738,14 @@ class ThemeProvider(object):
         return RemoveInactivePeriodForm()
 
     @LazyProperty
+    def statistics_form(self):
+        '''
+        :return: None, or an easywidgets Form to render on the user preferences page
+        '''
+        from allura.lib.widgets.forms import StatsPreferencesForm
+        return StatsPreferencesForm(action='/auth/prefs/set_statistics')
+
+    @LazyProperty
     def add_trove_category(self):
         '''
         :return: None, or an easywidgets Form to render on the page to create a

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/lib/widgets/forms.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index f76eff3..c1a1d75 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -451,6 +451,22 @@ class RemoveTimeSlotForm(ForgeForm):
         d['endtime'] = V.convertTime(kw.get('endtime',''))
         return d
 
+
+class StatsPreferencesForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        visible = ew.Checkbox(
+            label='Make my personal statistics visible to other users.')
+            
+    def display(self, **kw):
+        if kw.get('user').stats.visible:
+            self.fields['visible'].attrs = {'checked':'true'}      
+        else:
+            self.fields['visible'].attrs = {}    
+        return super(ForgeForm, self).display(**kw)
+                
+
 class RemoveTroveCategoryForm(ForgeForm):
     defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
index 8a71d86..d9cada2 100644
--- a/Allura/allura/model/contrib_stats.py
+++ b/Allura/allura/model/contrib_stats.py
@@ -21,6 +21,7 @@ class Stats(MappedClass):
 
     _id=FieldProperty(S.ObjectId)
 
+    visible = FieldProperty(bool, if_missing = True)
     registration_date = FieldProperty(datetime)
     general = FieldProperty([dict(
         category = S.ObjectId,

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
new file mode 100644
index 0000000..8159f52
--- /dev/null
+++ b/Allura/allura/templates/user_preferences.html
@@ -0,0 +1,287 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Settings</h2>
+    {{g.theme.personal_data_form.display(action="/auth/prefs/change_personal_data", user=c.user)}}
+  </div>
+
+  <div style="clear:both" class="grid-20">
+    <a name="Contacts"></a>
+    <h2>Personal Contacts</h2>
+    <h3>Skype account</h3>
+
+    {{g.theme.skype_account_form.display(action="/auth/prefs/skype_account",
+          initial_value=c.user.get_pref('skypeaccount'))}}
+
+    {%if c.user.get_pref('socialnetworks') or c.user.get_pref('telnumbers') or c.user.get_pref('webpages') %}
+      <h3>Other existing contacts</h3>
+        <table>
+          <tr>
+            <thead>
+              <th>Type</th>
+              <th>Contact</th>
+              <th>Actions</th>
+            </thead>
+          </tr>
+          {% for sn in c.user.get_pref('socialnetworks') %}
+             {{g.theme.remove_socialnetwork_form.display(account=sn.accounturl, socialnetwork=sn.socialnetwork)}}
+          {% endfor %}
+
+          {% for tn in c.user.get_pref('telnumbers') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_telnumber", value=tn, label="Telephone number")}}
+          {%endfor%}
+
+          {% for ws in c.user.get_pref('webpages') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_webpage", value=ws, label="Website url")}}
+          {%endfor%}
+        </table>
+    {% endif %}
+
+    <h3>Add a social network account</h3>
+    {{g.theme.add_socialnetwork_form.display(action="/auth/prefs/add_social_network")}}
+    <h3>Add a telephone number</h3>
+    {{g.theme.add_telnumber_form.display(action="/auth/prefs/add_telnumber")}}
+    <h3>Add a personal website</h3>
+    {{g.theme.add_website_form.display(action="/auth/prefs/add_webpage")}}
+  </div>
+
+  <a name="Availability"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Availability</h2>
+    <div class="grid-18">
+      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge.
+      Please, set your time intervals choosing a weekday and entering the time interval according to the timezone specified in your
+      personal data, using the format HH:MM. If you didn't set any timezone, your timeslots could be meaningless to other users,
+      therefore they will be ignored.
+    </div>
+    <div class="grid-18">
+      You can also specify periods of time during which you won't be able to work on the forge, in orther to communicate other users
+      that they can't contact you during those days. Please, do it specifying date intervals in format DD/MM/YYYY.
+    </div>
+  </div>
+  <div class="grid-20">
+    {%if c.user.get_availability_timeslots() %}
+      <h3>Existing availability timeslots</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Weekday</th>
+            <th>Start time</th>
+            <th>End time</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ts in c.user.get_availability_timeslots() %}
+          {{g.theme.remove_timeslot_form.display(
+                action="/auth/prefs/remove_timeslot",
+                weekday=ts.week_day,
+                starttime=ts.start_time,
+                endtime=ts.end_time)}}
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new availability timeslot</h3>
+    {{g.theme.add_timeslot_form.display(action="/auth/prefs/add_timeslot")}}
+  </div>
+
+  <div class="grid-20">
+    {%if c.user.get_inactive_periods() %}
+      <h3>Existing periods of inactivity on the forge</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Start date</th>
+            <th>End date</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ip in c.user.get_inactive_periods() %}
+          {{g.theme.remove_inactive_period_form.display(
+                action="/auth/prefs/remove_inactive_period",
+                startdate=ip.start_date,
+                enddate=ip.end_date)}}
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new period of inactivity on the forge</h3>
+    {{g.theme.add_inactive_period_form.display(action="/auth/prefs/add_inactive_period")}}
+  </div>
+
+  <div class="grid-20">
+    <h2>Skills list</h2>
+    <ul><li><a href="/auth/prefs/user_skills">Click here to check and change your skills list</a></li></ul>
+  </div>
+
+  {% if g.show_userstats %}
+    <a name="Statistics"></a>
+    <div class="grid-20">
+      <h2>Contribution statistics</h2>
+      {{g.theme.statistics_form.display(user=c.user)}}
+    </div>
+  {% endif %}
+
+  {% if g.theme.password_change_form %}
+  <div class="grid-20">
+    <h2>Change Password</h2>
+    {{ g.theme.password_change_form.display() }}
+  </div>
+  {% endif %}
+  {% if g.theme.upload_key_form %}
+  <div class="grid-20">
+    <h2>Upload ssh public key</h2>
+    {{ g.theme.upload_key_form.display() }}
+  </div>
+  {% endif %}
+
+  {% if tg.config.get('auth.method', 'local') == 'local' %}
+      <br style="clear:both"/>
+      <h2>API Token</h2>
+      {% if api_token %}
+        <p>
+          <b>API Key:</b><br/>
+          {{api_token.api_key}}<br/>
+          <b>Secret Key:</b><br/>
+          {{api_token.secret_key}}<br/>
+        </p>
+        <form method="POST" action="del_api_token" class="grid-18">
+          <input type="submit" value="Delete API Token">
+        </form>
+      {% else %}
+        <p>No API token generated</p>
+      {% endif %}
+      <form method="POST" action="gen_api_token" class="grid-18">
+        <input type="submit" value="(Re)generate API Token">
+      </form>
+  {% endif %}
+
+  <div style="clear:both"></div>
+
+  <h2>Authorized Third-party Applications</h2>
+  {% for access_tok in authorized_applications %}
+    <div>
+      <h3>{{access_tok.consumer_token.name}}</h3>
+      {{access_tok.consumer_token.description_html}}
+      {{ c.revoke_access.display(value=access_tok) }}
+      <br style="clear:both"/>
+  </div>
+ {% endfor %}
+    {% if not authorized_applications %}<p>No authorized third-party applications</p>{% endif %}
+
+
+  <h2>Subscriptions</h2>
+  {% if subscriptions %}
+    <p><em>Mark tools that you want to subscribe to. Unmark tools that you want to unsubscribe from. Press 'Save' button.</em></p>
+    {{c.form.display(action='update_subscriptions', value=dict(subscriptions=subscriptions))}}
+  {% else%}
+    <p>No subscriptions.</p>
+  {% endif %}
+  <hr/>
+  <div style="clear:both">&nbsp;</div>
+  <form action="update" method="post">
+        {% if tg.config.get('auth.method', 'local') == 'local' %}
+        <label class="grid-4">Display Name</label>
+        <div class="grid-18">
+          <input name="preferences.display_name" value="{{c.user.display_name}}" type="text">
+        </div>
+        {% endif %}
+        <label class="grid-4">Email Format</label>
+        <div class="grid-18">
+          <select name="preferences.email_format">
+            <option value="plain" {{'selected' if c.user.preferences.email_format == 'plain' else ''}}>Plain Text</option>
+            <option value="html" {{'selected' if c.user.preferences.email_format == 'html' else ''}}>HTML</option>
+            <option value="both" {{'selected' if c.user.preferences.email_format == 'both' else ''}}>Combined</option>
+          </select>
+        </div>
+        {% if tg.config.get('auth.method', 'local') == 'local' %}
+        <label class="grid-4">Page Size</label>
+        <div class="grid-18">
+          <select name="preferences.results_per_page">
+            {% for per_page in [25, 50, 100, 250] %}
+                <option {% if per_page == c.user.preferences.results_per_page %}selected="selected"{% endif %}
+                   value="{{per_page}}">{{per_page}}</option>
+            {% endfor %}
+          </select>
+        </div>
+        {% endif %}
+
+    {% if tg.config.get('auth.method', 'local') == 'local' %}
+      {% for a in c.user.email_addresses %}
+        <input name="addr-{{loop.index0}}.ord" value="{{loop.index0}}" type="hidden"/>
+      {% endfor %}
+      {% if c.user.email_addresses %}
+        <h3 class="grid-18">Email Addresses</h3>
+        <table class="grid-18">
+          <tr>
+            <th>Primary?</th>
+            <th>Address</th>
+            <th>Confirmed</th>
+            <th></th>
+          </tr>
+          {% for a in c.user.email_addresses %}
+          <tr>
+            {% set obj = c.user.address_object(a) %}
+            <td>{{lib.radio_button('primary_addr', None, a, c.user.preferences.email_address)}}</td>
+            <td>{{a}}</td>
+            {% if obj %}
+            <td>
+              {% if obj.confirmed %}
+                yes
+              {% else %}
+                no (<a href="{{g.url('/auth/send_verification_link', a=a)}}">verify</a>)
+              {% endif %}
+            </td>
+            {% else %}
+              <td>Unknown addr obj {{a}}</td>
+            {% endif %}
+            <td>{{lib.submit_button('Delete', 'addr-%s.delete' % i)}}</td>
+          </tr>
+          {% endfor %}
+        </table>
+        {% endif %}
+        <div class="grid-18">
+        {{lib.text_field('new_addr.addr', 'New Email Address')}}
+        {{lib.submit_button('Claim Address', name='new_addr.claim')}}
+        </div>
+
+        {% if c.user.open_ids %}
+        <h3 class="grid-18">OpenIDs Claimed</h3>
+        <table class="grid-18">
+          <tr>
+            <th>OpenID</th>
+            <th></th>
+          </tr>
+          {% for oid in c.user.open_ids %}
+            {% set obj = c.user.openid_object(oid) %}
+          <tr>
+            <td>{{oid}}</td>
+            <td>{{lib.submit_button('Delete', 'oid-%s.delete' % loop.index0)}}</td>
+          </tr>
+          {% endfor %}
+        </table>
+        {% endif %}
+        <div class="grid-18">
+        <a href="/auth/claim_oid">Claim New OpenID</a>
+        </div>
+    {% endif %}
+    <div class="grid-18">
+    {{lib.submit_button('Save Changes')}}
+    </div>
+  </form>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index b84eb0f..3944557 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -5,6 +5,7 @@ from allura.controllers import BaseController
 import allura.model as M
 from allura.lib.graphics.graphic_methods import create_histogram, create_progress_bar
 from forgeuserstats.model.stats import UserStats
+from pylons import c
 
 class ForgeUserStatsController(BaseController):
 
@@ -33,6 +34,8 @@ class ForgeUserStatsController(BaseController):
         if not self.user: 
             return dict(user=None)
         stats = self.user.stats
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
 
         ret_dict = _getDataForCategory(None, stats)
         ret_dict['user'] = self.user
@@ -133,6 +136,8 @@ class ForgeUserStatsCatController(BaseController):
         if not self.user:
             return dict(user=None)
         stats = self.user.stats
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
         
         cat_id = None
         if self.category: 
@@ -156,6 +161,8 @@ class ForgeUserStatsMetricController(BaseController):
         if not self.user:
             return dict(user=None)
         stats = self.user.stats
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
         
         commits = stats.getCommitsByCategory()
         return dict(user = self.user,
@@ -166,6 +173,8 @@ class ForgeUserStatsMetricController(BaseController):
     def artifacts(self, **kw):
         if not self.user:
             return dict(user=None)
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
 
         stats = self.user.stats       
         artifacts = stats.getArtifactsByCategory(detailed=True)
@@ -176,6 +185,8 @@ class ForgeUserStatsMetricController(BaseController):
     def tickets(self, **kw):
         if not self.user: 
             return dict(user=None)
+        if (not stats.visible) and (c.user != self.user):
+            return dict(user=self.user)
 
         artifacts = self.user.stats.getTicketsByCategory()
         return dict(user = self.user, data = artifacts) 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/ForgeUserStats/forgeuserstats/templates/artifacts.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/artifacts.html b/ForgeUserStats/forgeuserstats/templates/artifacts.html
index 9492628..d6a01d6 100644
--- a/ForgeUserStats/forgeuserstats/templates/artifacts.html
+++ b/ForgeUserStats/forgeuserstats/templates/artifacts.html
@@ -9,7 +9,7 @@
 
 {% block content %}
 
-  {% if user %}
+  {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
       <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
     </div>
@@ -47,6 +47,19 @@
       </table>
     </div>
     {% endif %}
+  {% else %}
+    {% if not user.stats.visible %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        This user has set his or her preferences so that personal statistics are not visible
+        to other users of the forge.
+      </div>
+    {% else %}
+      <h2>Invalid user</h2>
+      <div class="grid-20"> 
+        You are looking for personal statistics of a user which doesn't exist on this forge. Check your url.
+      </div>
+    {% endif %}
   {% endif %}
 
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/ForgeUserStats/forgeuserstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/commits.html b/ForgeUserStats/forgeuserstats/templates/commits.html
index 12f9712..cec0ab7 100644
--- a/ForgeUserStats/forgeuserstats/templates/commits.html
+++ b/ForgeUserStats/forgeuserstats/templates/commits.html
@@ -9,7 +9,7 @@
 
 {% block content %}
 
-  {% if user %}
+  {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
       <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
     </div>
@@ -37,5 +37,18 @@
       </table>
     </div>
     {% endif %}
+  {% else %}
+    {% if not user.stats.visible %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        This user has set his or her preferences so that personal statistics are not visible
+        to other users of the forge.
+      </div>
+    {% else %}
+      <h2>Invalid user</h2>
+      <div class="grid-20"> 
+        You are looking for personal statistics of a user which doesn't exist on this forge. Check your url.
+      </div>
+    {% endif %}
   {% endif %}
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/ForgeUserStats/forgeuserstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/index.html b/ForgeUserStats/forgeuserstats/templates/index.html
index 37cd5da..4a8f504 100644
--- a/ForgeUserStats/forgeuserstats/templates/index.html
+++ b/ForgeUserStats/forgeuserstats/templates/index.html
@@ -11,7 +11,7 @@
 {% endblock %}
 
 {% block content %}
-  {% if user %}
+  {% if user and (user.stats.visible or (c.user == user)) %}
 
     {% if category %}
        <ul>
@@ -418,7 +418,18 @@
       </p>
     {% endif %}
   {% else %}
-    Invalid user!
+    {% if not user.stats.visible %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        This user has set his or her preferences so that personal statistics are not visible
+        to other users of the forge.
+      </div>
+    {% else %}
+      <h2>Invalid user</h2>
+      <div class="grid-20"> 
+        You are looking for personal statistics of a user which doesn't exist on this forge. Check your url.
+      </div>
+    {% endif %}
   {% endif %}
 
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fa9f88ba/ForgeUserStats/forgeuserstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/templates/tickets.html b/ForgeUserStats/forgeuserstats/templates/tickets.html
index 4604c16..6ee66d4 100644
--- a/ForgeUserStats/forgeuserstats/templates/tickets.html
+++ b/ForgeUserStats/forgeuserstats/templates/tickets.html
@@ -9,7 +9,7 @@
 
 {% block content %}
 
-  {% if user %}
+  {% if user and (user.stats.visible or (c.user == user)) %}
     <div class="grid-20">
       <ul><li><a href="/userstats/{{user.username}}">Go back to general statistics</a></li></ul>
     </div>
@@ -47,5 +47,18 @@
       </table>
     </div>
     {% endif %}
+  {% else %}
+    {% if not user.stats.visible %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        This user has set his or her preferences so that personal statistics are not visible
+        to other users of the forge.
+      </div>
+    {% else %}
+      <h2>Invalid user</h2>
+      <div class="grid-20"> 
+        You are looking for personal statistics of a user which doesn't exist on this forge. Check your url.
+      </div>
+    {% endif %}
   {% endif %}
 {% endblock %}


[08/48] git commit: [#5909] Added /auth/prefs redirect to /auth/subscriptions

Posted by tv...@apache.org.
[#5909] Added /auth/prefs redirect to /auth/subscriptions

Signed-off-by: Cory Johns <jo...@geek.net>


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

Branch: refs/heads/si/5453
Commit: 7eee5d9d998d9bbc1c151a42a16db75889d2f4a6
Parents: b55760a
Author: Cory Johns <jo...@geek.net>
Authored: Wed Mar 13 19:33:34 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:47 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py |    8 ++++++++
 1 files changed, 8 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7eee5d9d/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index e52ea5e..7abdbc8 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -67,6 +67,14 @@ class AuthController(BaseController):
         self.subscriptions = SubscriptionsController()
         self.oauth = OAuthController()
 
+    @expose()
+    def prefs(self, *args, **kwargs):
+        '''
+        Redirect old /auth/prefs URL to /auth/subscriptions
+        (to handle old email links, etc).
+        '''
+        redirect('subscriptions')
+
     @expose('jinja:allura:templates/login.html')
     @with_trailing_slash
     def index(self, *args, **kwargs):


[37/48] git commit: [5453] Fixed bug in code to hide stats

Posted by tv...@apache.org.
[5453] Fixed bug in code to hide stats


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

Branch: refs/heads/si/5453
Commit: c21e9f61e7cae66ee993ab53b17a766f443c8f33
Parents: f689ce0
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 23 12:06:17 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:55 2013 +0000

----------------------------------------------------------------------
 .../forgeuserstats/controllers/userstats.py        |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c21e9f61/ForgeUserStats/forgeuserstats/controllers/userstats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/controllers/userstats.py b/ForgeUserStats/forgeuserstats/controllers/userstats.py
index 3944557..c77dded 100644
--- a/ForgeUserStats/forgeuserstats/controllers/userstats.py
+++ b/ForgeUserStats/forgeuserstats/controllers/userstats.py
@@ -173,6 +173,7 @@ class ForgeUserStatsMetricController(BaseController):
     def artifacts(self, **kw):
         if not self.user:
             return dict(user=None)
+        stats = self.user.stats
         if (not stats.visible) and (c.user != self.user):
             return dict(user=self.user)
 
@@ -185,6 +186,7 @@ class ForgeUserStatsMetricController(BaseController):
     def tickets(self, **kw):
         if not self.user: 
             return dict(user=None)
+        stats = self.user.stats
         if (not stats.visible) and (c.user != self.user):
             return dict(user=self.user)
 


[45/48] git commit: [#5453] Setting userstats tool as anchored for user projects

Posted by tv...@apache.org.
[#5453] Setting userstats tool as anchored for user projects


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

Branch: refs/heads/si/5453
Commit: edb0bcfa0cf6e9db3fd8602436349ab07a9b8bd2
Parents: b0f9b70
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Mar 3 10:30:05 2013 +0100
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:56 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/app_globals.py    |    4 +---
 Allura/allura/model/auth.py         |    6 ++++--
 Allura/allura/model/project.py      |    2 --
 Allura/allura/websetup/bootstrap.py |    1 +
 Allura/development.ini              |    6 ------
 ForgeUserStats/test.ini             |    1 -
 6 files changed, 6 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index ee2c9b9..1ab9b31 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -172,12 +172,10 @@ class Globals(object):
         # Zarkov logger
         self._zarkov = None
 
-        self.show_userstats = config.get('userstats.enable','false')=='true'
         # Set listeners to update stats
         statslisteners = []
         for name, ep in self.entry_points['stats'].iteritems():
-            if config.get('%s.enable' % name,'false')=='true':
-                statslisteners.append(ep())
+            statslisteners.append(ep())
         self.statsUpdater = PostEvent(statslisteners)
 
     @LazyProperty

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 23f5366..a0bdb20 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -341,9 +341,11 @@ class User(MappedClass, ActivityNode, ActivityObject):
 
     @property
     def stats(self):
-        if g.show_userstats:
+        if 'userstats' in g.entry_points['stats']:
             from forgeuserstats.model.stats import UserStats
-            return UserStats.query.get(_id=self.stats_id)
+            if self.stats_id:
+                return UserStats.query.get(_id=self.stats_id)
+            return UserStats.create(self)
         else: 
             return None
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index ab47f61..9f07f67 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -715,8 +715,6 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                         ('admin', 'admin', 'Admin'),
                         ('search', 'search', 'Search'),
                         ('activity', 'activity', 'Activity')]
-                if g.show_userstats:
-                    apps = apps + [('userstats', 'userstats', 'Statistics')]
             else:
                 apps = [('admin', 'admin', 'Admin'),
                         ('search', 'search', 'Search'),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/Allura/allura/websetup/bootstrap.py
----------------------------------------------------------------------
diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py
index 9614b99..747504c 100644
--- a/Allura/allura/websetup/bootstrap.py
+++ b/Allura/allura/websetup/bootstrap.py
@@ -88,6 +88,7 @@ def bootstrap(command, conf, vars):
                                               google_analytics = False))
     n_users = M.Neighborhood(name='Users', url_prefix='/u/',
                              shortname_prefix='u/',
+                             anchored_tools='userstats:Statistics',
                              features=dict(private_projects = True,
                                            max_projects = None,
                                            css = 'none',

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 68821e4..3dc76d0 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -126,12 +126,6 @@ scm.repos.tarball.url_prefix = http://localhost/
 
 trovecategories.enableediting = true
 
-# If set to false, the stats of the user are not
-# updated and they are not shown to users.
-# Note: the name of the parameter has to be the same
-#       of the entry point, followed by .enable
-userstats.enable = true
-
 # ActivityStream
 activitystream.master = mongodb://127.0.0.1:27017
 activitystream.database = activitystream

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/edb0bcfa/ForgeUserStats/test.ini
----------------------------------------------------------------------
diff --git a/ForgeUserStats/test.ini b/ForgeUserStats/test.ini
index 04c1c6e..6753aa4 100644
--- a/ForgeUserStats/test.ini
+++ b/ForgeUserStats/test.ini
@@ -5,7 +5,6 @@
 #
 [DEFAULT]
 debug = true
-userstats.enable = true
 
 [server:main]
 use = egg:Paste#http


[39/48] git commit: Fix matplotlib version; 1.1.1rc no longer on pypi

Posted by tv...@apache.org.
Fix matplotlib version; 1.1.1rc no longer on pypi


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

Branch: refs/heads/si/5453
Commit: e72efa866f9ef8603c7339bae9ae4062e5fc0c4c
Parents: c21e9f6
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Mon Feb 25 21:55:16 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Thu Mar 21 14:20:55 2013 +0000

----------------------------------------------------------------------
 requirements-common.txt |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/e72efa86/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index 9ca2a92..518081c 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -48,7 +48,7 @@ WebOb==1.0.8
 # part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
 wsgiref==0.1.2
 numpy==1.6.1
-matplotlib==1.1.1rc
+matplotlib==1.1.1
 
 # tg2 deps (not used directly)
 Babel==0.9.6