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

[01/50] [abbrv] git commit: Don't run tests twice on 42cc buildbot

Updated Branches:
  refs/heads/db/5453 2b3bad7d7 -> d7fc0ac55 (forced update)


Don't run tests twice on 42cc buildbot


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

Branch: refs/heads/db/5453
Commit: c0f67e3184b1a1d299b794e2bdecd2a2aa9a26e5
Parents: e9ad55a
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Apr 3 12:55:43 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 14:49:42 2013 +0000

----------------------------------------------------------------------
 Makefile              |    8 ++++++++
 Makefile.def.buildbot |    9 +--------
 2 files changed, 9 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c0f67e31/Makefile
----------------------------------------------------------------------
diff --git a/Makefile b/Makefile
index dfe1ea4..12c9048 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,14 @@ PID_PATH?=.
 
 # Targets
 test:
+ifdef BB
+# running on buildbot (Makefile.def.buildbot sets BB to 1)
+# rebuild apps
+	./rebuild-all.bash
+# setup pysvn
+	-[ ! -f $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn ] && ln -s /usr/lib64/python2.7/site-packages/pysvn $(VIRTUAL_ENV)/lib/python2.7/site-packages/
+	-[ ! -d $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn-1.7.5-py2.7.egg-info ] && mkdir $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn-1.7.5-py2.7.egg-info
+endif
 	ALLURA_VALIDATION=none ./run_tests
 	./run_clonedigger
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c0f67e31/Makefile.def.buildbot
----------------------------------------------------------------------
diff --git a/Makefile.def.buildbot b/Makefile.def.buildbot
index f88ae0e..82179d7 100644
--- a/Makefile.def.buildbot
+++ b/Makefile.def.buildbot
@@ -1,8 +1 @@
-test: buildbot_test
-
-buildbot_test:
-	./rebuild-all.bash
-	-[ ! -f $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn ] && ln -s /usr/lib64/python2.7/site-packages/pysvn $(VIRTUAL_ENV)/lib/python2.7/site-packages/
-	-[ ! -d $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn-1.7.5-py2.7.egg-info ] && mkdir $(VIRTUAL_ENV)/lib/python2.7/site-packages/pysvn-1.7.5-py2.7.egg-info
-	ALLURA_VALIDATION=none ./run_tests
-	./run_clonedigger
+BB=1


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

Posted by br...@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/4c65ef1e
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4c65ef1e
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4c65ef1e

Branch: refs/heads/db/5453
Commit: 4c65ef1e7813f32e7cc5f53c3f71eda50e91f65f
Parents: c5dbe23
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 10 15:41:14 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/4c65ef1e/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)


[47/50] [abbrv] git commit: Remove user_preferences.html

Posted by br...@apache.org.
Remove user_preferences.html


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

Branch: refs/heads/db/5453
Commit: 4b4c2f4820c09b13d9e0d20b3f92fc871b6a6ced
Parents: 2cfc060
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Mar 24 11:36:25 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:59 2013 +0000

----------------------------------------------------------------------
 Allura/allura/templates/user_preferences.html |  279 --------------------
 1 files changed, 0 insertions(+), 279 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4b4c2f48/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 768cd2e..0000000
--- a/Allura/allura/templates/user_preferences.html
+++ /dev/null
@@ -1,279 +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>
-    <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.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 %}


[07/50] [abbrv] git commit: [#5879] Fixed error from all_commit_ids on empty git repo

Posted by br...@apache.org.
[#5879] Fixed error from all_commit_ids on empty git repo

Signed-off-by: Cory Johns <cj...@slashdotmedia.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/0531982a
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0531982a
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0531982a

Branch: refs/heads/db/5453
Commit: 0531982a667b46d561f478e6f7685999f96aa5ab
Parents: db0d23f
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Thu Mar 28 21:35:36 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Wed Apr 3 17:26:03 2013 +0000

----------------------------------------------------------------------
 ForgeGit/forgegit/model/git_repo.py |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0531982a/ForgeGit/forgegit/model/git_repo.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/model/git_repo.py b/ForgeGit/forgegit/model/git_repo.py
index 299ddb2..e7d529f 100644
--- a/ForgeGit/forgegit/model/git_repo.py
+++ b/ForgeGit/forgegit/model/git_repo.py
@@ -161,6 +161,8 @@ class GitImplementation(M.RepositoryImplementation):
         """Yield commit ids, starting with the head(s) of the commit tree and
         ending with the root (first commit).
         """
+        if not self._git.head.is_valid():
+            return  # empty repo
         seen = set()
         for ci in self._git.iter_commits(all=True, topo_order=True):
             if ci.binsha in seen: continue


[14/50] [abbrv] git commit: [5453] Added some tests

Posted by br...@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/1c0a770d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/1c0a770d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/1c0a770d

Branch: refs/heads/db/5453
Commit: 1c0a770dc15cac92bfe847ac3531307bbf4fab5b
Parents: 297bea0
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 19:07:48 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1c0a770d/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
 


[27/50] [abbrv] git commit: [5453] Fixed stats template and improved controller

Posted by br...@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/2215a9a9
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/2215a9a9
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/2215a9a9

Branch: refs/heads/db/5453
Commit: 2215a9a9b81b236575884e8da421819560c9d625
Parents: 0659e4c
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 16:21:49 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/2215a9a9/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/2215a9a9/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>


[23/50] [abbrv] git commit: [5453] Removed .svn

Posted by br...@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/e161df48
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/e161df48
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/e161df48

Branch: refs/heads/db/5453
Commit: e161df48242f9597761741f81e1ea50b7936e466
Parents: 4e138d9
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 18 18:20:56 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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/e161df48/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 %}


[19/50] [abbrv] git commit: [5453] Removed loops through entry points

Posted by br...@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/0000281b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0000281b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0000281b

Branch: refs/heads/db/5453
Commit: 0000281bce89bc7a5f0bc88ab6ef818a76db6301
Parents: e63fe31
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 19 17:29:34 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/0000281b/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index beaddb6..d23891c 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/0000281b/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index ed62dfb..b8240b9 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -37,13 +37,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'.
@@ -341,16 +334,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)
 


[43/50] [abbrv] git commit: [#5453] Fix initialization of the stats object

Posted by br...@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/1f2a58b8
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/1f2a58b8
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/1f2a58b8

Branch: refs/heads/db/5453
Commit: 1f2a58b8aab3cabbe8e98fc6d1c44b305f7a94b4
Parents: e718c03
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Mar 6 22:39:25 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/1f2a58b8/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:


[44/50] [abbrv] git commit: [#5453] Correctly initialize user stats

Posted by br...@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/0c9b5c26
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0c9b5c26
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0c9b5c26

Branch: refs/heads/db/5453
Commit: 0c9b5c26df7cbee7a98eb8b41e2a642138f52d4a
Parents: 1f2a58b
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Mar 7 23:44:50 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/0c9b5c26/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:


[12/50] [abbrv] git commit: [5453] Logins count

Posted by br...@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/c5dbe230
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/c5dbe230
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/c5dbe230

Branch: refs/heads/db/5453
Commit: c5dbe2300b48b95feff960722af1b58394b5f832
Parents: 727f1e9
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 9 09:19:56 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/c5dbe230/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index a3d01b8..61debc9 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()


[18/50] [abbrv] git commit: [5453] Added unit tests to userstats

Posted by br...@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/8b7803a9
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/8b7803a9
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/8b7803a9

Branch: refs/heads/db/5453
Commit: 8b7803a9578b1f19597d80c84f2b3cc992951cda
Parents: 0000281
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Jan 20 16:33:25 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/8b7803a9/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/8b7803a9/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/8b7803a9/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/8b7803a9/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/8b7803a9/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/8b7803a9/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
-


[46/50] [abbrv] git commit: [#5453] make Profile tool be anchored before userstats, and fixes:

Posted by br...@apache.org.
[#5453] make Profile tool be anchored before userstats, and fixes:

* make profile icon show up properly everywhere
* make profile tool show up in menus properly
* remove special-casing for the profile tool being first; relies
  on anchored feature now
* update first_mount() to remove simple case (no auth check), to check
  is_visible_to so it is consistent with other menu checks, and to
  remove invalid old ForgeWikiApp type check


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

Branch: refs/heads/db/5453
Commit: 359f10042d86b2a15cb4dcb2bff9ed8ef7344ae6
Parents: 667f2c0
Author: Dave Brondsema <db...@geek.net>
Authored: Mon Mar 25 16:09:11 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:59 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/project.py               |    6 +--
 Allura/allura/ext/user_profile/user_main.py        |   17 ++++++---
 Allura/allura/model/project.py                     |   28 ++++-----------
 Allura/allura/nf/allura/css/allura.css             |    6 ++--
 Allura/allura/nf/allura/css/site_style.css         |    2 +-
 .../allura/tests/functional/test_neighborhood.py   |    2 +-
 Allura/allura/websetup/bootstrap.py                |    2 +-
 7 files changed, 27 insertions(+), 36 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 7a1551f..891e18f 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -105,7 +105,7 @@ class NeighborhoodController(object):
         if self.neighborhood.redirect:
             redirect(self.neighborhood.redirect)
         if not self.neighborhood.has_home_tool:
-            mount = c.project.first_mount()
+            mount = c.project.ordered_mounts()[0]
             if mount is not None:
                 if 'ac' in mount:
                     redirect(mount['ac'].options.mount_point + '/')
@@ -323,7 +323,7 @@ class ProjectController(object):
     @expose()
     @with_trailing_slash
     def index(self, **kw):
-        mount = c.project.first_mount('read')
+        mount = c.project.first_mount_visible(c.user)
         activity_enabled = config.get('activitystream.enabled', False)
         activity_enabled = request.cookies.get('activitystream.enabled', activity_enabled)
         activity_enabled = asbool(activity_enabled)
@@ -334,8 +334,6 @@ class ProjectController(object):
                 redirect(mount['ac'].options.mount_point + '/')
             elif 'sub' in mount:
                 redirect(mount['sub'].url())
-        elif c.project.app_instance('profile'):
-            redirect('profile/')
         else:
             redirect(c.project.app_configs[0].options.mount_point + '/')
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/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..907271a 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -9,7 +9,7 @@ from tg import expose, redirect, validate, response
 from webob import exc
 
 from allura import version
-from allura.app import Application
+from allura.app import Application, SitemapEntry
 from allura.lib import helpers as h
 from allura.lib.helpers import DateTimeConverter
 from allura.lib.security import require_access
@@ -23,10 +23,11 @@ log = logging.getLogger(__name__)
 class UserProfileApp(Application):
     __version__ = version.__version__
     installable = False
+    tool_label = 'Profile'
     icons={
-        24:'images/sftheme/24x24/home_24.png',
-        32:'images/sftheme/32x32/home_32.png',
-        48:'images/sftheme/48x48/home_48.png'
+        24:'images/home_24.png',
+        32:'images/home_32.png',
+        48:'images/home_48.png'
     }
 
     def __init__(self, user, config):
@@ -38,11 +39,17 @@ class UserProfileApp(Application):
     @property
     @h.exceptionless([], log)
     def sitemap(self):
-        return []
+        return [SitemapEntry('Profile', '.')]
 
     def admin_menu(self):
         return []
 
+    def main_menu(self):
+        return [SitemapEntry('Profile', '.')]
+
+    def is_visible_to(self, user):
+        return True
+
     def install(self, project):
         pr = c.user.project_role()
         if pr:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 39509ff..b0b45df 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -402,11 +402,6 @@ class Project(MappedClass, ActivityNode, ActivityObject):
         delta_ordinal = i
         max_ordinal = i
 
-        if self.is_user_project:
-            entries.append({'ordinal': delta_ordinal, 'entry':SitemapEntry('Profile', "%sprofile/" % self.url(), ui_icon="tool-home")})
-            max_ordinal = delta_ordinal
-            delta_ordinal = delta_ordinal + 1
-
         for sub in self.direct_subprojects:
             ordinal = sub.ordinal + delta_ordinal
             if ordinal > max_ordinal:
@@ -620,26 +615,17 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                 result.append({'ordinal': int(ordinal), 'ac': ac, 'rank': rank})
         return sorted(result, key=lambda e: (e['ordinal'], e['rank']))
 
-    def first_mount(self, required_access=None):
-        '''Returns the first (toolbar order) mount, or the first mount to
-        which the user has the required access.'''
-        from forgewiki.wiki_main import ForgeWikiApp
+    def first_mount_visible(self, user):
         mounts = self.ordered_mounts()
-        if self.is_user_project:
-            for mount in mounts:
-                if 'ac' in mount and mount['ac'].tool_name == 'profile':
-                    return mount
-        if mounts and required_access is None:
-            return mounts[0]
         for mount in mounts:
             if 'sub' in mount:
-                obj = mount['sub']
+                sub = mount['sub']
+                if has_access(sub, 'read', user):
+                    return mount
             elif 'ac' in mount:
-                obj = self.app_instance(mount['ac'])
-            else:
-                continue
-            if has_access(obj, required_access) or isinstance(obj, ForgeWikiApp):
-                return mount
+                app = self.app_instance(mount['ac'])
+                if app.is_visible_to(user):
+                    return mount
         return None
 
     def next_mount_point(self, include_hidden=False):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/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 f3863b8..613caa7 100644
--- a/Allura/allura/nf/allura/css/allura.css
+++ b/Allura/allura/nf/allura/css/allura.css
@@ -36,7 +36,7 @@ b.ico.ico-vote-up { background-image: url('../images/vote_up.png'); }
 b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 
 
-.ui-icon-tool-home {
+.ui-icon-tool-home, .ui-icon-tool-profile {
   background-image: url("../images/home_24.png");
   background-repeat: no-repeat;
 }
@@ -106,7 +106,7 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-repeat: no-repeat;
 }
 
-#top_nav .ui-icon-tool-home {
+#top_nav .ui-icon-tool-home, #top_nav .ui-icon-tool-profile {
   background-image: url("../images/home_32.png");
 }
 #top_nav .ui-icon-tool-wiki {
@@ -149,7 +149,7 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-image: url("../images/chat_32.png");
 }
 
-.big_icon.ui-icon-tool-home {
+.big_icon.ui-icon-tool-home, .big_icon.ui-icon-tool-profile {
   background-image: url("../images/home_48.png");
 }
 .big_icon.ui-icon-tool-wiki {

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/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 cc503f4..2f96715 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -2216,7 +2216,7 @@ div.attachment_thumb .file_type span {
 }
 
 /* forge tool icons */
-.ui-icon-tool-home {
+.ui-icon-tool-home, .ui-icon-tool-profile {
   background-repeat: no-repeat;
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/Allura/allura/tests/functional/test_neighborhood.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index 815f557..f1d7e33 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -806,7 +806,7 @@ class TestNeighborhood(TestController):
     @td.with_user_project('test-user')
     def test_profile_topnav_menu(self):
         r = self.app.get('/u/test-user/', extra_environ=dict(username='test-user')).follow()
-        assert '<a href="/u/test-user/profile/" class="ui-icon-tool-home">' in r
+        assert '<a href="/u/test-user/profile/" class="ui-icon-tool-profile">' in r, r
 
     def test_user_project_creates_on_demand(self):
         M.User.register(dict(username='donald-duck'), make_project=False)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/359f1004/Allura/allura/websetup/bootstrap.py
----------------------------------------------------------------------
diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py
index 747504c..90c8e62 100644
--- a/Allura/allura/websetup/bootstrap.py
+++ b/Allura/allura/websetup/bootstrap.py
@@ -88,7 +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',
+                             anchored_tools='profile:Profile,userstats:Statistics',
                              features=dict(private_projects = True,
                                            max_projects = None,
                                            css = 'none',


[11/50] [abbrv] git commit: [5453] Added some tests

Posted by br...@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/dfc0c763
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/dfc0c763
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/dfc0c763

Branch: refs/heads/db/5453
Commit: dfc0c763bf347d79c609b0fc9ce75a48d8499101
Parents: 1c0a770
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 19:08:03 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/dfc0c763/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/dfc0c763/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


[31/50] [abbrv] git commit: [5453] Correct .ini file for tests in ForgeUserStats

Posted by br...@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/11b065c0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/11b065c0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/11b065c0

Branch: refs/heads/db/5453
Commit: 11b065c062c6169a30ce99077a90b943bbe67d15
Parents: f9d42ac
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Feb 1 21:25:01 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/11b065c0/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


[08/50] [abbrv] [5453] adding support for user stats

Posted by br...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index eded3e8..55697b2 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


[40/50] [abbrv] git commit: [#5453] Remove old stats.py file, improve userstats code

Posted by br...@apache.org.
[#5453] Remove old stats.py file, improve userstats 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/2cfc0608
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/2cfc0608
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/2cfc0608

Branch: refs/heads/db/5453
Commit: 2cfc060827c36478c18d54f7176dc461d8039cc6
Parents: 2df588d
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Mar 24 11:00:39 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py                  |   12 -
 Allura/allura/controllers/site_admin.py            |   24 -
 .../ext/user_profile/templates/user_index.html     |    9 -
 Allura/allura/lib/plugin.py                        |   14 +
 Allura/allura/model/contrib_stats.py               |  637 --------------
 Allura/allura/model/stats.py                       |  640 ++++++++++++++-
 Allura/allura/templates/site_admin.html            |    1 -
 Allura/allura/tests/functional/test_site_admin.py  |   15 -
 ForgeUserStats/forgeuserstats/model/stats.py       |    9 +-
 .../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 ---
 21 files changed, 652 insertions(+), 1174 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 1b9c74e..8e18767 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -726,18 +726,6 @@ class SubscriptionsController(BaseController):
     @h.vardec
     @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/2cfc0608/Allura/allura/controllers/site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/site_admin.py b/Allura/allura/controllers/site_admin.py
index 096aba7..6d8fdd8 100644
--- a/Allura/allura/controllers/site_admin.py
+++ b/Allura/allura/controllers/site_admin.py
@@ -53,30 +53,6 @@ class SiteAdminController(object):
         neighborhoods.sort(key=lambda n:n[0])
         return dict(neighborhoods=neighborhoods)
 
-    @expose('jinja:allura:templates/site_admin_stats.html')
-    @without_trailing_slash
-    def stats(self, limit=25):
-        stats = defaultdict(lambda:defaultdict(list))
-        agg_timings = defaultdict(list)
-        for doc in M.Stats.m.find():
-            if doc.url.startswith('/_debug'): continue
-            doc_stats = stats[doc.url]
-            for t,val in doc.timers.iteritems():
-                doc_stats[t].append(val)
-                agg_timings[t].append(val)
-        for url, timings in stats.iteritems():
-            new_timings = dict(
-                (timer, round(sum(readings)/len(readings),3))
-                for timer, readings in timings.iteritems())
-            timings.update(new_timings)
-        agg_timings = dict(
-            (timer, round(sum(readings)/len(readings),3))
-            for timer, readings in agg_timings.iteritems())
-        stats = sorted(stats.iteritems(), key=lambda x:-x[1]['total'])
-        return dict(
-            agg_timings=agg_timings,
-            stats=stats[:int(limit)])
-
     @expose('jinja:allura:templates/site_admin_api_tickets.html')
     @without_trailing_slash
     def api_tickets(self, **data):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/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 2b17b79..2614953 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -236,15 +236,6 @@
     </div>
   </div>
 
-  {% 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="{{c.project.url()}}userstats"/>Go to the personal statistics of this user</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/2cfc0608/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index ed704d4..efc5d07 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -174,6 +174,15 @@ class AuthenticationProvider(object):
     def update_notifications(self, user):
         raise NotImplemented, 'update_notifications'
 
+    def user_registration_date(self, user):
+        '''
+        Returns the date in which a user registered himself/herself on the forge.
+
+        :param user: a :class:`User <allura.model.auth.User>`
+        :rtype: :class:`datetime <datetime.datetime>`
+        '''
+        raise NotImplementedError, 'user_registration_date'
+
 class LocalAuthenticationProvider(AuthenticationProvider):
     '''
     Stores user passwords on the User model, in mongo.  Uses per-user salt and
@@ -230,6 +239,11 @@ class LocalAuthenticationProvider(AuthenticationProvider):
     def update_notifications(self, user):
         return ''
 
+    def user_registration_date(self, user):
+        if user._id:
+            return user._id.generation_time
+        return datetime.utcnow()
+
 class LdapAuthenticationProvider(AuthenticationProvider):
     def register_user(self, user_doc):
         from allura import model as M

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/Allura/allura/model/contrib_stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/contrib_stats.py b/Allura/allura/model/contrib_stats.py
deleted file mode 100644
index 3799ea5..0000000
--- a/Allura/allura/model/contrib_stats.py
+++ /dev/null
@@ -1,637 +0,0 @@
-import pymongo
-from pylons import tmpl_context as c, app_globals as g
-from pylons import 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='basestats'
-        session = main_orm_session
-        unique_indexes = [ '_id']
-
-    _id=FieldProperty(S.ObjectId)
-
-    visible = FieldProperty(bool, if_missing = True)
-    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=[],
-                        tickets=dict(
-                            assigned=0,
-                            solved=0,
-                            revoked=0,
-                            totsolvingtime=0))
-                    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/2cfc0608/Allura/allura/model/stats.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/stats.py b/Allura/allura/model/stats.py
index ee1f297..df4e45a 100644
--- a/Allura/allura/model/stats.py
+++ b/Allura/allura/model/stats.py
@@ -1,13 +1,637 @@
-import logging
+import pymongo
+from pylons import tmpl_context as c, app_globals as g
+from pylons import request
 
-import ming
+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 .session import main_doc_session
+from allura.model.session import main_orm_session
+from allura.lib import helpers as h
 
-log = logging.getLogger(__name__)
-
-class Stats(ming.Document):
+class Stats(MappedClass):
     class __mongometa__:
-        session = main_doc_session
-        name='stats'
+        name='basestats'
+        session = main_orm_session
+        unique_indexes = [ '_id']
+
+    _id=FieldProperty(S.ObjectId)
+
+    visible = FieldProperty(bool, if_missing = True)
+    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):
+        res = self.query.find()
+        n = res.count()
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getCodeContribution() for x in res])
+        averagecontribution=sum([x.getCodeContribution() for x in res]) / n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageDiscussionContribution(self):
+        res = self.query.find()
+        n = res.count()
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getDiscussionContribution() for x in res])
+        averagecontribution=sum([x.getDiscussionContribution() for x in res])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    @classmethod
+    def getMaxAndAverageTicketsSolvingPercentage(self):
+        res = self.query.find()
+        n = res.count()
+        if n == 0:
+            return 0, 0
+        maxcontribution=max([x.getTicketsContribution() for x in res])
+        averagecontribution=sum([x.getTicketsContribution() for x in res])/n
+        return maxcontribution, round(averagecontribution, 2)
+
+    def codeRanking(self):
+        res = self.query.find()
+        totn = res.count()
+        codcontr = self.getCodeContribution()
+        upper = len([x for x in res if x.getCodeContribution() > codcontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def discussionRanking(self):
+        res = self.query.find()
+        totn = res.count()
+        disccontr = self.getDiscussionContribution()
+        upper=len([x for x in res if x.getDiscussionContribution()>disccontr])
+        return round((totn - upper) * 100.0 / totn, 2)
+
+    def ticketsRanking(self):
+        res = self.query.find()
+        totn = res.count()
+        ticketscontr = self.getTicketsContribution()
+        upper=len([x for x in res 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=[],
+                        tickets=dict(
+                            assigned=0,
+                            solved=0,
+                            revoked=0,
+                            totsolvingtime=0))
+                    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/2cfc0608/Allura/allura/templates/site_admin.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/site_admin.html b/Allura/allura/templates/site_admin.html
index b8b1b7d..5ba439b 100644
--- a/Allura/allura/templates/site_admin.html
+++ b/Allura/allura/templates/site_admin.html
@@ -16,7 +16,6 @@
   <div>&nbsp;</div>
   <ul>
     <li class="{{page=='index' and 'active' or ''}}"><a href="{{sidebar_rel}}."><b data-icon="{{g.icons['admin'].char}}" class="ico {{g.icons['admin'].css}}"></b>Home</a></li>
-    <li class="{{page=='stats' and 'active' or ''}}"><a href="{{sidebar_rel}}stats"><b data-icon="{{g.icons['stats'].char}}" class="ico {{g.icons['stats'].css}}"></b>Stats</a></li>
     <li class="{{page=='api_tickets' and 'active' or ''}}"><a href="{{sidebar_rel}}api_tickets"><b data-icon="{{g.icons['admin'].char}}" class="ico {{g.icons['admin'].css}}"></b>API Tickets</a></li>
     <li class="{{page=='add_subscribers' and 'active' or ''}}"><a href="{{sidebar_rel}}add_subscribers"><b data-icon="{{g.icons['admin'].char}}" class="ico {{g.icons['admin'].css}}"></b>Add Subscribers</a></li>
     <li class="{{page=='new_projects' and 'active' or ''}}"><a href="{{sidebar_rel}}new_projects"><b data-icon="{{g.icons['admin'].char}}" class="ico {{g.icons['admin'].css}}"></b>New Projects</a></li>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/Allura/allura/tests/functional/test_site_admin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_site_admin.py b/Allura/allura/tests/functional/test_site_admin.py
index b5f2d10..3e1925a 100644
--- a/Allura/allura/tests/functional/test_site_admin.py
+++ b/Allura/allura/tests/functional/test_site_admin.py
@@ -23,21 +23,6 @@ class TestSiteAdmin(TestController):
         cells = stats_table.findAll('td')
         assert cells[0].contents[0] == 'Adobe', cells[0].contents[0]
 
-    def test_performance(self):
-        r = self.app.get('/nf/admin/stats', extra_environ=dict(
-                username='test-user'), status=403)
-        r = self.app.get('/nf/admin/stats', extra_environ=dict(
-                username='root'))
-        assert 'Forge Site Admin' in r.html.find('h2',{'class':'dark title'}).contents[0]
-        stats_table = r.html.find('table')
-        headers = stats_table.findAll('th')
-        assert headers[0].contents[0] == 'Url'
-        assert headers[1].contents[0] == 'Ming'
-        assert headers[2].contents[0] == 'Mongo'
-        assert headers[3].contents[0] == 'Render'
-        assert headers[4].contents[0] == 'Template'
-        assert headers[5].contents[0] == 'Total Time'
-
     def test_tickets_access(self):
         r = self.app.get('/nf/admin/api_tickets', extra_environ=dict(
                 username='test-user'), status=403)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index b2f70a4..722b8d0 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -2,9 +2,11 @@ from ming.orm import FieldProperty
 from ming import schema as S
 from datetime import datetime, timedelta
 from ming.orm import session, Mapper
+from pylons import request
 
+from allura.lib import plugin
 from allura.model.session import main_orm_session
-from allura.model.contrib_stats import Stats
+from allura.model import Stats
 
 class UserStats(Stats):
     class __mongometa__:
@@ -19,11 +21,12 @@ class UserStats(Stats):
 
     @classmethod
     def create(cls, user):
+        auth_provider = plugin.AuthenticationProvider.get(request)
+        reg_date = auth_provider.user_registration_date(user)
         stats = cls.query.get(user_id = user._id)
         if stats:
             return stats
-        stats = cls(user_id=user._id,
-            registration_date = datetime.utcnow())
+        stats = cls(user_id=user._id, registration_date = reg_date)
         user.stats_id = stats._id
         return stats
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/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
deleted file mode 100755
index 8b2a2fe..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/applypatch-msg.sample
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 6ef1d29..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/commit-msg.sample
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 2266821..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-commit.sample
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 0f7a148..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive
+++ /dev/null
@@ -1 +0,0 @@
-post-receive

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/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
deleted file mode 100755
index 7a83e17..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-receive.sample
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 5323b56..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/post-update.sample
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index b1f187c..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-applypatch.sample
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 439eefd..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-commit.sample
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index be1b06e..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/pre-rebase.sample
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 3652424..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/prepare-commit-msg.sample
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/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/2cfc0608/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
deleted file mode 100755
index 4ea5e4d..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update
+++ /dev/null
@@ -1 +0,0 @@
-update

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/2cfc0608/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
deleted file mode 100755
index fd63b2d..0000000
--- a/ForgeUserStats/forgeuserstats/tests/data/testgit.git/hooks/update.sample
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/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


[16/50] [abbrv] git commit: [5453] fixed bug

Posted by br...@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/08fc1450
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/08fc1450
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/08fc1450

Branch: refs/heads/db/5453
Commit: 08fc1450e34d771d36fa810fa246a74cbe5c4cea
Parents: 4c65ef1
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 10 19:25:44 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/08fc1450/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)


[32/50] [abbrv] git commit: [5453] Allowing to hide stats

Posted by br...@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/8479c2d1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/8479c2d1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/8479c2d1

Branch: refs/heads/db/5453
Commit: 8479c2d1bc345724ba6526c24924490ba4d4c501
Parents: 11b065c
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 16 13:01:52 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/8479c2d1/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 8e18767..1118d86 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/8479c2d1/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/8479c2d1/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index ed704d4..59f03e5 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -736,6 +736,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/8479c2d1/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/8479c2d1/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/8479c2d1/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/8479c2d1/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/8479c2d1/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/8479c2d1/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/8479c2d1/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/8479c2d1/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 %}


[04/50] [abbrv] git commit: [#5961] Added "skipped" state for tasks

Posted by br...@apache.org.
[#5961] Added "skipped" state for tasks

Signed-off-by: Cory Johns <cj...@slashdotmedia.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/c7366893
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/c7366893
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/c7366893

Branch: refs/heads/db/5453
Commit: c736689303917fd5d334df4e5bbef530cf723983
Parents: 1d70c92
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Wed Apr 3 15:04:33 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Apr 3 16:39:10 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7366893/Allura/allura/model/monq_model.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/monq_model.py b/Allura/allura/model/monq_model.py
index bdb1d90..a97f16f 100644
--- a/Allura/allura/model/monq_model.py
+++ b/Allura/allura/model/monq_model.py
@@ -24,7 +24,7 @@ class MonQTask(MappedClass):
     Properties
 
         - _id - bson.ObjectId() for this task
-        - state - 'ready', 'busy', 'error', or 'complete' task status
+        - state - 'ready', 'busy', 'error', 'complete', or 'skipped' task status
         - priority - integer priority, higher is more priority
         - result_type - either 'keep' or 'forget', what to do with the task when
           it's done
@@ -38,7 +38,7 @@ class MonQTask(MappedClass):
         - kwargs - **kwargs to be sent to the task function
         - result - if the task is complete, the return value. If in error, the traceback.
     '''
-    states = ('ready', 'busy', 'error', 'complete')
+    states = ('ready', 'busy', 'error', 'complete', 'skipped')
     result_types = ('keep', 'forget')
     class __mongometa__:
         session = task_orm_session


[35/50] [abbrv] git commit: Fix matplotlib version; 1.1.1rc no longer on pypi

Posted by br...@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/4fcb8a0c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4fcb8a0c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4fcb8a0c

Branch: refs/heads/db/5453
Commit: 4fcb8a0c2bb04687ffe8aabb7ad9b3068a546e47
Parents: c5c4305
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Mon Feb 25 21:55:16 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:15 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4fcb8a0c/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index 55697b2..e072ad9 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


[24/50] [abbrv] git commit: [5453] Improved tests

Posted by br...@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/4e138d9b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4e138d9b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4e138d9b

Branch: refs/heads/db/5453
Commit: 4e138d9b0c9654a36cb9ff182f3e12c1695b1a4d
Parents: 89ee255
Author: Stefano Invernizzi <st...@apache.org>
Authored: Mon Jan 14 14:43:56 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/4e138d9b/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/4e138d9b/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


[09/50] [abbrv] git commit: [5453] adding support for user stats

Posted by br...@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/7dae679d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/7dae679d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/7dae679d

Branch: refs/heads/db/5453
Commit: 7dae679d2a11a80597b41c1f8909186d9656309c
Parents: 5eb6d36
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Dec 12 22:06:15 2012 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:09 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                |    8 +
 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, 3056 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7dae679d/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/7dae679d/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 5ac4767..beaddb6 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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index aa3ddfa..b3760dd 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -347,7 +347,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:
@@ -372,6 +372,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/7dae679d/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 5ccfd60..8bcf950 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
@@ -36,6 +37,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'.
@@ -332,6 +340,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
@@ -578,6 +593,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/7dae679d/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 2ea6f4c..b9bad98 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/7dae679d/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index f15ec81..7087edf 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -111,6 +111,14 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             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', len(commit_ids), all_commits, new_clone)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 686fd7d..0bd6a43 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -444,6 +444,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:
@@ -456,6 +458,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(
@@ -468,7 +473,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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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/7dae679d/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
+


[50/50] [abbrv] git commit: [#5453] don't count svn imports either

Posted by br...@apache.org.
[#5453] don't count svn imports either


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

Branch: refs/heads/db/5453
Commit: d7fc0ac5537736a743f4bfe8fd7c30d13e9a3914
Parents: 359f100
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Thu Apr 4 18:36:39 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Thu Apr 4 18:36:39 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/d7fc0ac5/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index d06e879..20383e9 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -112,7 +112,7 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             if (i+1) % 100 == 0:
                 log.info('Compute last commit info %d: %s', (i+1), ci._id)
 
-    if not all_commits:
+    if not all_commits and not new_clone:
         for commit in commit_ids:
             new = repo.commit(commit)
             user = User.by_email_address(new.committed.email)


[39/50] [abbrv] git commit: [#5453] Fix svn test to work with user stats

Posted by br...@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/ae1b1482
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/ae1b1482
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/ae1b1482

Branch: refs/heads/db/5453
Commit: ae1b14820b897cd398547f2cc79a6efe41aed094
Parents: 7a51977
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 12 20:04:01 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/ae1b1482/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/ae1b1482/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 89964a7..9731ae2 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()


[06/50] [abbrv] git commit: [#5879] Fixed tests failing due to new commit in test repo

Posted by br...@apache.org.
[#5879] Fixed tests failing due to new commit in test repo

Signed-off-by: Cory Johns <cj...@slashdotmedia.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/5eb6d368
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/5eb6d368
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/5eb6d368

Branch: refs/heads/db/5453
Commit: 5eb6d368d458e45a69ed3d15d1d9a9bd1f18e3b9
Parents: 0531982
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Thu Mar 28 21:35:51 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Wed Apr 3 17:26:03 2013 +0000

----------------------------------------------------------------------
 .../forgegit/tests/functional/test_controllers.py  |    6 +++---
 ForgeGit/forgegit/tests/model/test_repository.py   |    2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/5eb6d368/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 1c8e0df..5be4d01 100644
--- a/ForgeGit/forgegit/tests/functional/test_controllers.py
+++ b/ForgeGit/forgegit/tests/functional/test_controllers.py
@@ -81,14 +81,14 @@ class TestRootController(_TestCase):
     def test_commit_browser_data(self):
         resp = self.app.get('/src-git/commit_browser_data')
         data = json.loads(resp.body);
-        assert data['max_row'] == 3
+        assert data['max_row'] == 4
         assert data['next_column'] == 1
         assert_equal(data['built_tree']['df30427c488aeab84b2352bdf88a3b19223f9d7a'],
                 {u'url': u'/p/test/src-git/ci/df30427c488aeab84b2352bdf88a3b19223f9d7a/',
                  u'oid': u'df30427c488aeab84b2352bdf88a3b19223f9d7a',
                  u'column': 0,
                  u'parents': [u'6a45885ae7347f1cac5103b0050cc1be6a1496c8'],
-                 u'message': u'Add README', u'row': 1})
+                 u'message': u'Add README', u'row': 2})
 
     def test_log(self):
         resp = self.app.get('/src-git/ci/1e146e67985dcd71c74de79613719bef7bddca4a/log/')
@@ -184,7 +184,7 @@ class TestRootController(_TestCase):
 
     def test_refresh(self):
         notification = M.Notification.query.find(
-            dict(subject='[test:src-git] 4 new commits to test Git')).first()
+            dict(subject='[test:src-git] 5 new commits to test Git')).first()
         domain = '.'.join(reversed(c.app.url[1:-1].split('/'))).replace('_', '-')
         common_suffix = tg.config.get('forgemail.domain', '.sourceforge.net')
         email = 'noreply@%s%s' % (domain, common_suffix)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/5eb6d368/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 1102ee8..bce1a4f 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -56,7 +56,7 @@ class TestNewGit(unittest.TestCase):
         assert self.rev.tree._id == self.rev.tree_id
         assert self.rev.summary == self.rev.message.splitlines()[0]
         assert self.rev.shorthand_id() == '[1e146e]'
-        assert self.rev.symbolic_ids == (['master'], [])
+        assert self.rev.symbolic_ids == (['master', 'zz'], [])
         assert self.rev.url() == (
             '/p/test/src-git/ci/'
             '1e146e67985dcd71c74de79613719bef7bddca4a/')


[30/50] [abbrv] git commit: [5453] correct links in userstats

Posted by br...@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/f9d42aca
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/f9d42aca
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/f9d42aca

Branch: refs/heads/db/5453
Commit: f9d42aca7de89df77667c8b2e8eae039920d364e
Parents: 05c2bd4
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Jan 31 15:57:12 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/f9d42aca/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>


[03/50] [abbrv] git commit: [#4329] Change repo_refreshed event params

Posted by br...@apache.org.
[#4329] Change repo_refreshed event params

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/c06eeda2
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/c06eeda2
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/c06eeda2

Branch: refs/heads/db/5453
Commit: c06eeda207244d4f10cb65a84a067d9db0b3ae78
Parents: cb210e9
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Mon Apr 1 14:35:41 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 16:24:06 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c06eeda2/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index ca0c43b..d42def3 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -113,10 +113,8 @@ def refresh_repo(repo, all_commits=False, notify=True):
 
 
     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))
+    g.post_event('repo_refreshed', len(commit_ids), all_commits)
+
     # Send notifications
     if notify:
         send_notifications(repo, commit_ids)


[33/50] [abbrv] git commit: Fix pylons globals imports

Posted by br...@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/4327087d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4327087d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4327087d

Branch: refs/heads/db/5453
Commit: 4327087d52ac8f9a6f8f8129f93c7bf384e7515c
Parents: 4fcb8a0
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Mon Feb 25 21:55:33 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:15 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/4327087d/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/4327087d/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/50] [abbrv] git commit: [5453] Fixed bug in code to hide stats

Posted by br...@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/c5c43058
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/c5c43058
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/c5c43058

Branch: refs/heads/db/5453
Commit: c5c43058b43ae7d4f9c3b9a1a056a86c7d5e59eb
Parents: 9855a97
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 23 12:06:17 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:15 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/c5c43058/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)
 


[05/50] [abbrv] git commit: [#5879] Fixed ordering of all_commit_ids for ForgeGit to ensure repo root always comes last

Posted by br...@apache.org.
[#5879] Fixed ordering of all_commit_ids for ForgeGit to ensure repo root always comes last

The performance of refresh_last_commits depends on the repo root being
processed first and the commits following in roughly story (a.k.a.
topological) order.  If the commits are processed out of order, it has
to walk back up the tree a *lot* more to reconstruct last-commit data
that it otherwise gets for "free" (having already computed it).

Signed-off-by: Cory Johns <cj...@slashdotmedia.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/db0d23fc
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/db0d23fc
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/db0d23fc

Branch: refs/heads/db/5453
Commit: db0d23fc0662641ab63147cf9e40619a74894410
Parents: c736689
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Thu Mar 28 19:09:46 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Wed Apr 3 17:26:02 2013 +0000

----------------------------------------------------------------------
 ForgeGit/forgegit/model/git_repo.py                |    9 ++++-----
 .../5c/47243c8e424136fd5cdd18cd94d34c66d1955c      |    3 +++
 .../5c/891311e2b402f3f86eabf1688b5496183d9f94      |  Bin 0 -> 24 bytes
 .../b1/42cbffd81acd7c57b47bf64fbb9e0920d82c55      |  Bin 0 -> 78 bytes
 .../forgegit/tests/data/testgit.git/refs/heads/zz  |    1 +
 ForgeGit/forgegit/tests/model/test_repository.py   |   11 +++++++++++
 6 files changed, 19 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/model/git_repo.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/model/git_repo.py b/ForgeGit/forgegit/model/git_repo.py
index d7c9d49..299ddb2 100644
--- a/ForgeGit/forgegit/model/git_repo.py
+++ b/ForgeGit/forgegit/model/git_repo.py
@@ -162,11 +162,10 @@ class GitImplementation(M.RepositoryImplementation):
         ending with the root (first commit).
         """
         seen = set()
-        for head in self._git.heads:
-            for ci in self._git.iter_commits(head, topo_order=True):
-                if ci.binsha in seen: continue
-                seen.add(ci.binsha)
-                yield ci.hexsha
+        for ci in self._git.iter_commits(all=True, topo_order=True):
+            if ci.binsha in seen: continue
+            seen.add(ci.binsha)
+            yield ci.hexsha
 
     def new_commits(self, all_commits=False):
         graph = {}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/47243c8e424136fd5cdd18cd94d34c66d1955c
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/47243c8e424136fd5cdd18cd94d34c66d1955c b/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/47243c8e424136fd5cdd18cd94d34c66d1955c
new file mode 100644
index 0000000..18fe11f
--- /dev/null
+++ b/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/47243c8e424136fd5cdd18cd94d34c66d1955c
@@ -0,0 +1,3 @@
+x��M
+�0�]���i�Ӏ��΅wHf&�b:%�oo��o�����PJ��;��I�S�4����d|���w��h��Z���K��8v>����Fo�}pz�:$�>a4Q�w���M��2-��������D�
+�O(�zp�7Z�n���~������W�*��"�L�
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/891311e2b402f3f86eabf1688b5496183d9f94
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/891311e2b402f3f86eabf1688b5496183d9f94 b/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/891311e2b402f3f86eabf1688b5496183d9f94
new file mode 100644
index 0000000..5337266
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testgit.git/objects/5c/891311e2b402f3f86eabf1688b5496183d9f94 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/tests/data/testgit.git/objects/b1/42cbffd81acd7c57b47bf64fbb9e0920d82c55
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testgit.git/objects/b1/42cbffd81acd7c57b47bf64fbb9e0920d82c55 b/ForgeGit/forgegit/tests/data/testgit.git/objects/b1/42cbffd81acd7c57b47bf64fbb9e0920d82c55
new file mode 100644
index 0000000..0e707ac
Binary files /dev/null and b/ForgeGit/forgegit/tests/data/testgit.git/objects/b1/42cbffd81acd7c57b47bf64fbb9e0920d82c55 differ

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/tests/data/testgit.git/refs/heads/zz
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/data/testgit.git/refs/heads/zz b/ForgeGit/forgegit/tests/data/testgit.git/refs/heads/zz
new file mode 100644
index 0000000..68c205a
--- /dev/null
+++ b/ForgeGit/forgegit/tests/data/testgit.git/refs/heads/zz
@@ -0,0 +1 @@
+5c47243c8e424136fd5cdd18cd94d34c66d1955c

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/db0d23fc/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index fbcfaa0..1102ee8 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -247,6 +247,17 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
         self.repo.tarball('HEAD')
         assert os.path.isfile("/tmp/tarball/git/t/te/test/testgit.git/test-src-git-HEAD.tar.gz")
 
+    def test_all_commit_ids(self):
+        cids = list(self.repo.all_commit_ids())
+        heads = [
+                '1e146e67985dcd71c74de79613719bef7bddca4a',  # master
+                '5c47243c8e424136fd5cdd18cd94d34c66d1955c',  # zz
+            ]
+        self.assertIn(cids[0], heads)  # repo head comes first
+        for head in heads:
+            self.assertIn(head, cids)  # all branches included
+        self.assertEqual(cids[-1], '9a7df788cf800241e3bb5a849c8870f2f8259d98')  # repo root comes last
+
     def test_ls(self):
         lcd_map = self.repo.commit('HEAD').tree.ls()
         self.assertEqual(lcd_map, [{


[10/50] [abbrv] git commit: [5453] Adding tests and fixing errors related to commit stats

Posted by br...@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/89ee2555
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/89ee2555
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/89ee2555

Branch: refs/heads/db/5453
Commit: 89ee25554cb307d3936d24fa82fa3760edb69a62
Parents: dfc0c76
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 12 18:37:26 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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/89ee2555/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


[29/50] [abbrv] git commit: [5453] removed duplicated links

Posted by br...@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/05c2bd45
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/05c2bd45
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/05c2bd45

Branch: refs/heads/db/5453
Commit: 05c2bd45e54ccc0dab009622061d5c3e971fe34d
Parents: b63c3de
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:44:14 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/05c2bd45/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/05c2bd45/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 %}


[22/50] [abbrv] git commit: [5453] Removed wrong message in userstats Web page

Posted by br...@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/0659e4c3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0659e4c3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0659e4c3

Branch: refs/heads/db/5453
Commit: 0659e4c3c76be41c1c0b8e7e61631ef52c1e90cf
Parents: 25cfbdd
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 15:10:59 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/0659e4c3/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 %}


[20/50] [abbrv] git commit: [5453] Moved loop through stats listeners

Posted by br...@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/e63fe314
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/e63fe314
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/e63fe314

Branch: refs/heads/db/5453
Commit: e63fe31412d5cfde05000850c3b0e98d6bc14edf
Parents: e161df4
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Jan 19 17:11:42 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/e63fe314/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/e63fe314/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/e63fe314/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 61debc9..ed704d4 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/e63fe314/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index b3760dd..7628620 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -374,13 +374,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/e63fe314/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 8bcf950..ed62dfb 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -594,8 +594,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/e63fe314/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 46e952a..799d3cc 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -118,8 +118,7 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
         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('repo_refreshed', len(commit_ids), all_commits, new_clone)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/e63fe314/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 0bd6a43..c491d7b 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -444,8 +444,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:
@@ -458,9 +457,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(
@@ -474,8 +473,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


[15/50] [abbrv] git commit: [5453] Fixed bug in user stats

Posted by br...@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/297bea05
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/297bea05
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/297bea05

Branch: refs/heads/db/5453
Commit: 297bea054410f207fcf86d6f77b5a5b0fd91e4f4
Parents: 08fc145
Author: Stefano Invernizzi <st...@apache.org>
Authored: Fri Jan 11 17:19:53 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/297bea05/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)


[45/50] [abbrv] git commit: [#5453] Fixed bug in userstats model

Posted by br...@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/7a519772
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/7a519772
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/7a519772

Branch: refs/heads/db/5453
Commit: 7a5197724256a14f3fc149e51a5c5b8aa22527b9
Parents: 0c9b5c2
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Mar 9 16:34:02 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/7a519772/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):


[48/50] [abbrv] git commit: [#5453] only count commits for incremental commits, not full refreshes

Posted by br...@apache.org.
[#5453] only count commits for incremental commits, not full refreshes


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

Branch: refs/heads/db/5453
Commit: d62f37d005b77c419ddfcaac5131c438c548587b
Parents: 4b4c2f4
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Wed Mar 27 21:04:59 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:59 2013 +0000

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


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/d62f37d0/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 799d3cc..d06e879 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -112,13 +112,14 @@ def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
             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:
-            g.statsUpdater.newCommit(new, repo.app_config.project, user)
+    if not all_commits:
+        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:
+                g.statsUpdater.newCommit(new, repo.app_config.project, user)
 
     log.info('Refresh complete for %s', repo.full_fs_path)
     g.post_event('repo_refreshed', len(commit_ids), all_commits, new_clone)


[25/50] [abbrv] git commit: [5453] Improved code

Posted by br...@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/9855a974
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/9855a974
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/9855a974

Branch: refs/heads/db/5453
Commit: 9855a974b11b58944e632c14c84818f7c9fb683b
Parents: 8479c2d
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sat Feb 16 14:33:22 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/9855a974/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/9855a974/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/9855a974/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/9855a974/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/9855a974/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


[41/50] [abbrv] git commit: [#5453] Avoid conflict with mongo's built-in stats collection

Posted by br...@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/e718c033
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/e718c033
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/e718c033

Branch: refs/heads/db/5453
Commit: e718c033bb1abd6cf8453376288733de369eb54a
Parents: b2299db
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 5 21:45:38 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/e718c033/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']
 


[02/50] [abbrv] git commit: [#4329] Denote whether refresh is for a new clone/fork

Posted by br...@apache.org.
[#4329] Denote whether refresh is for a new clone/fork

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/1d70c920
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/1d70c920
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/1d70c920

Branch: refs/heads/db/5453
Commit: 1d70c9200541727aadb32c16bc0955f36d30c286
Parents: c06eeda
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Apr 2 15:34:08 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 16:24:06 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/repo_refresh.py |    4 ++--
 Allura/allura/model/repository.py   |    6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1d70c920/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index d42def3..f15ec81 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
 
 QSIZE=100
 
-def refresh_repo(repo, all_commits=False, notify=True):
+def refresh_repo(repo, all_commits=False, notify=True, new_clone=False):
     all_commit_ids = commit_ids = list(repo.all_commit_ids())
     if not commit_ids:
         # the repo is empty, no need to continue
@@ -113,7 +113,7 @@ def refresh_repo(repo, all_commits=False, notify=True):
 
 
     log.info('Refresh complete for %s', repo.full_fs_path)
-    g.post_event('repo_refreshed', len(commit_ids), all_commits)
+    g.post_event('repo_refreshed', len(commit_ids), all_commits, new_clone)
 
     # Send notifications
     if notify:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/1d70c920/Allura/allura/model/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 6ca3a31..9019375 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -314,7 +314,7 @@ class Repository(Artifact, ActivityObject):
         self._impl.clone_from(source)
         log.info('... %r cloned', self)
         g.post_event('repo_cloned', source_url, source_path)
-        self.refresh(notify=False)
+        self.refresh(notify=False, new_clone=True)
 
     def log(self, branch='master', offset=0, limit=10):
         return list(self._log(branch, offset, limit))
@@ -483,7 +483,7 @@ class Repository(Artifact, ActivityObject):
     def unknown_commit_ids(self):
         return unknown_commit_ids_repo(self.all_commit_ids())
 
-    def refresh(self, all_commits=False, notify=True):
+    def refresh(self, all_commits=False, notify=True, new_clone=False):
         '''Find any new commits in the repository and update'''
         try:
             log.info('... %r analyzing', self)
@@ -491,7 +491,7 @@ class Repository(Artifact, ActivityObject):
             session(self).flush(self)
             self._impl.refresh_heads()
             if asbool(tg.config.get('scm.new_refresh')):
-                refresh_repo(self, all_commits, notify)
+                refresh_repo(self, all_commits, notify, new_clone)
             for head in self.heads + self.branches + self.repo_tags:
                 ci = self.commit(head.object_id)
                 if ci is not None:


[37/50] [abbrv] git commit: [#5453] Setting userstats tool as anchored for user projects

Posted by br...@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/437f3548
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/437f3548
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/437f3548

Branch: refs/heads/db/5453
Commit: 437f354884acb47c3c90b470dce95063110e6554
Parents: 6bb90c4
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Mar 3 10:30:05 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52: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/437f3548/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/437f3548/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index b8240b9..a6904f2 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -342,9 +342,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/437f3548/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 0f46a11..39509ff 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -715,8 +715,6 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                 apps += [('Wiki', 'wiki', 'Wiki'),
                         ('profile', 'profile', 'Profile'),
                        ]
-                if g.show_userstats:
-                    apps = apps + [('userstats', 'userstats', 'Statistics')]
             apps += [
                 ('admin', 'admin', 'Admin'),
                 ('search', 'search', 'Search'),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/437f3548/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/437f3548/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/437f3548/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


[38/50] [abbrv] git commit: [#5453] Make image assets local to tool

Posted by br...@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/b2299dbd
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/b2299dbd
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/b2299dbd

Branch: refs/heads/db/5453
Commit: b2299dbd06303e43c13cf0ad930bf8f556b9eacb
Parents: 437f354
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Mar 5 21:44:54 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:57 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/b2299dbd/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/b2299dbd/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/b2299dbd/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/b2299dbd/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


[42/50] [abbrv] git commit: [#5453] Fixing errors in userstats

Posted by br...@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/2df588d1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/2df588d1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/2df588d1

Branch: refs/heads/db/5453
Commit: 2df588d1868e4c263fdecf0725ef11442ab46359
Parents: ae1b148
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Mar 13 01:25:10 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:58 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/2df588d1/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/2df588d1/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()


[13/50] [abbrv] git commit: [5453] added missing import

Posted by br...@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/727f1e91
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/727f1e91
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/727f1e91

Branch: refs/heads/db/5453
Commit: 727f1e919c4dca4b89222ed10cf5c2005c2879ef
Parents: 7dae679
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Dec 12 22:32:29 2012 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:12 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/727f1e91/Allura/allura/model/repo_refresh.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repo_refresh.py b/Allura/allura/model/repo_refresh.py
index 7087edf..46e952a 100644
--- a/Allura/allura/model/repo_refresh.py
+++ b/Allura/allura/model/repo_refresh.py
@@ -18,6 +18,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__)
 


[49/50] [abbrv] git commit: [#5453] make tool_label match, so tool sorting is blocked when userstats is installed as an anchored tool

Posted by br...@apache.org.
[#5453] make tool_label match, so tool sorting is blocked when userstats is installed as an anchored 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/667f2c01
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/667f2c01
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/667f2c01

Branch: refs/heads/db/5453
Commit: 667f2c01077b1f66c6edb5b67ad89bc69d585a82
Parents: d62f37d
Author: Dave Brondsema <db...@slashdotmedia.com>
Authored: Thu Mar 28 14:07:28 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:52:59 2013 +0000

----------------------------------------------------------------------
 ForgeUserStats/forgeuserstats/main.py |    6 +++---
 1 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/667f2c01/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
index acc196d..194c7d6 100644
--- a/ForgeUserStats/forgeuserstats/main.py
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -45,7 +45,7 @@ class UserStatsListener(EventsListener):
         if not stats:
             stats = UserStats.create(user)
 
-        if event_type == "assigned": 
+        if event_type == "assigned":
             stats.addAssignedTicket(ticket.mod_date, project)
         elif event_type == "revoked":
             stats.addRevokedTicket(ticket.mod_date, project)
@@ -71,7 +71,7 @@ class UserStatsListener(EventsListener):
 
 class ForgeUserStatsApp(Application):
     __version__ = version.__version__
-    tool_label='Stats'
+    tool_label='UserStats'
     default_mount_label='Stats'
     default_mount_point='stats'
     permissions = ['configure', 'read', 'write',
@@ -128,7 +128,7 @@ class ForgeUserStatsApp(Application):
         return links
 
     def install(self, project):
-        #It doesn't make any sense to install the tool twice on the same 
+        #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:


[26/50] [abbrv] git commit: [5453] Implemented loop to load other listeners updating stats

Posted by br...@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/4a38f1c7
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4a38f1c7
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4a38f1c7

Branch: refs/heads/db/5453
Commit: 4a38f1c78650c4c9951e776e0359906d35c0f19d
Parents: 2215a9a
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:03:56 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/4a38f1c7/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/4a38f1c7/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


[36/50] [abbrv] git commit: [#5453] Moving userstats into a tool

Posted by br...@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/6bb90c4b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/6bb90c4b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/6bb90c4b

Branch: refs/heads/db/5453
Commit: 6bb90c4b29705e4d021c814c3f534cdd743843db
Parents: 4327087
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Feb 27 23:47:24 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:49:58 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/6bb90c4b/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 1118d86..1b9c74e 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/6bb90c4b/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index d23891c..c56e373 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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 59f03e5..ed704d4 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -736,14 +736,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/6bb90c4b/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/6bb90c4b/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 39509ff..0f46a11 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -715,6 +715,8 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                 apps += [('Wiki', 'wiki', 'Wiki'),
                         ('profile', 'profile', 'Profile'),
                        ]
+                if g.show_userstats:
+                    apps = apps + [('userstats', 'userstats', 'Statistics')]
             apps += [
                 ('admin', 'admin', 'Admin'),
                 ('search', 'search', 'Search'),

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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/6bb90c4b/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
+
       """,
       )


[28/50] [abbrv] git commit: [5453] correct typo error

Posted by br...@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/b63c3de1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/b63c3de1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/b63c3de1

Branch: refs/heads/db/5453
Commit: b63c3de1683bacd93352ca05ae5ad0baeffbdda1
Parents: 4a38f1c
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Jan 30 17:25:36 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:14 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/b63c3de1/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/50] [abbrv] git commit: [5453] Improved functional tests

Posted by br...@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/25cfbdd8
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/25cfbdd8
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/25cfbdd8

Branch: refs/heads/db/5453
Commit: 25cfbdd84d095d3af12c025158457a3de90c38ec
Parents: 8b7803a
Author: Stefano Invernizzi <st...@apache.org>
Authored: Tue Jan 22 11:20:26 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Apr 3 21:45:13 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/25cfbdd8/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")