Return-Path: X-Original-To: apmail-incubator-allura-commits-archive@minotaur.apache.org Delivered-To: apmail-incubator-allura-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 36299F9F3 for ; Thu, 4 Apr 2013 18:38:03 +0000 (UTC) Received: (qmail 42855 invoked by uid 500); 4 Apr 2013 18:38:03 -0000 Delivered-To: apmail-incubator-allura-commits-archive@incubator.apache.org Received: (qmail 42779 invoked by uid 500); 4 Apr 2013 18:38:03 -0000 Mailing-List: contact allura-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: allura-dev@incubator.apache.org Delivered-To: mailing list allura-commits@incubator.apache.org Received: (qmail 42594 invoked by uid 99); 4 Apr 2013 18:38:02 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 04 Apr 2013 18:38:02 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 5DD8A837C92; Thu, 4 Apr 2013 18:38:02 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: brondsem@apache.org To: allura-commits@incubator.apache.org Date: Thu, 04 Apr 2013 18:38:07 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [06/43] 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/68b8dfe2 Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/68b8dfe2 Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/68b8dfe2 Branch: refs/heads/master Commit: 68b8dfe21f772e5ee3f677ae58369af599911a81 Parents: 709f4bf Author: Stefano Invernizzi Authored: Wed Dec 12 22:06:15 2012 +0100 Committer: Dave Brondsema Committed: Thu Apr 4 18:37:33 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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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 @@ + {% if statslinkurl %} +
+
User statistics
+ +
+ {% endif %} + {% if c.user.username == user.username %}
Email Addresses http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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/68b8dfe2/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 +