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

[13/42] git commit: [5453] Added unit tests to userstats

[5453] Added unit tests to userstats


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

Branch: refs/heads/db/5453
Commit: 169e9d98e8709a105ffae0e4bbd22b4e842e1b71
Parents: 0bd0617
Author: Stefano Invernizzi <st...@apache.org>
Authored: Sun Jan 20 16:33:25 2013 +0100
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Thu Mar 28 16:53:31 2013 +0000

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


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

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

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

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

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

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