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:11 UTC
[09/50] [abbrv] git commit: [5453] adding support for user stats
[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
+