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/03/28 17:57:58 UTC

[29/42] git commit: [#5453] Moving userstats into a tool

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

Branch: refs/heads/db/5453
Commit: 350e1bef97f22d908336b3cea3bfec7ae9736c85
Parents: 0a7c119
Author: Stefano Invernizzi <st...@apache.org>
Authored: Wed Feb 27 23:47:24 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Thu Mar 28 16:53:33 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/350e1bef/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/350e1bef/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index a387b98..83aa5e5 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -69,9 +69,7 @@ class RootController(WsgiDispatchController):
         if n and not n.url_prefix.startswith('//'):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()
-        ep = g.entry_points["stats"].get('userstats')
-        if ep and g.show_userstats:
-            self.userstats = ep().root
+
         super(RootController, self).__init__()
 
     def _setup_request(self):

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

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