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

[2/5] organization and organization stats

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/model/.svn/text-base/stats.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/model/.svn/text-base/stats.py.svn-base b/ForgeOrganizationStats/forgeorganizationstats/model/.svn/text-base/stats.py.svn-base
new file mode 100644
index 0000000..f434e4e
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/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/787ef235/ForgeOrganizationStats/forgeorganizationstats/model/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/model/__init__.py b/ForgeOrganizationStats/forgeorganizationstats/model/__init__.py
new file mode 100644
index 0000000..0a92f1e
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/model/__init__.py
@@ -0,0 +1 @@
+from orgstats import OrganizationStats

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/model/orgstats.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/model/orgstats.py b/ForgeOrganizationStats/forgeorganizationstats/model/orgstats.py
new file mode 100644
index 0000000..f9c0cca
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/model/orgstats.py
@@ -0,0 +1,62 @@
+from ming.orm import FieldProperty
+from ming import schema as S
+from datetime import datetime, timedelta
+from ming.orm import session, Mapper
+
+from allura.model.session import main_orm_session
+
+from allura.model import Stats
+
+class OrganizationStats(Stats):
+    class __mongometa__:
+        name='organizationstats'
+        session = main_orm_session
+        unique_indexes = [ '_id', 'organization_id']
+
+    organization_id = FieldProperty(S.ObjectId)
+
+    @classmethod
+    def create(cls, organization):
+        stats = cls(organization_id=organization._id,
+            registration_date = datetime.utcnow())
+        organization.stats_id = stats._id
+        return stats
+
+    def getLastMonthCommitsPerMember(self, category = None):
+        from forgeorganization.organization.model import Organization
+
+        org = Organization.query.get(_id=self.organization_id)
+        members = len(org.getEnrolledUsers())
+        if not members: 
+            return dict(number=0.0, lines=0.0)
+        commits = self.getLastMonthCommits(category=category)
+        return dict(
+            number=round(float(commits['number'])/members,2), 
+            lines=round(float(commits['lines'])/members,2))
+
+    def getLastMonthArtifactsPerMember(self, category = None, art_type = None):
+        from forgeorganization.organization.model import Organization
+
+        org = Organization.query.get(_id=self.organization_id)
+        members = len(org.getEnrolledUsers())
+        if not members: 
+            return dict(number=0.0, lines=0.0)
+        artifacts = self.getLastMonthArtifacts(category=category)
+        return dict(
+            created=round(float(artifacts['created'])/members,2), 
+            modified=round(float(artifacts['modified'])/members,2))
+
+    def getLastMonthTicketsPerMember(self, category = None):
+        from forgeorganization.organization.model import Organization
+
+        org = Organization.query.get(_id=self.organization_id)
+        members = len(org.getEnrolledUsers())
+        if not members: 
+            return dict(number=0.0, lines=0.0)
+        tickets = self.getLastMonthTickets(category=category)
+        return dict(
+            assigned=round(float(tickets['assigned'])/members,2), 
+            solved=round(float(tickets['solved'])/members,2), 
+            revoked=round(float(tickets['revoked'])/members,2))
+
+Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/all-wcprops b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/all-wcprops
new file mode 100644
index 0000000..efae2aa
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/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/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/entries b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/entries
new file mode 100644
index 0000000..ef7dfdb
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/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
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/artifacts.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/artifacts.html.svn-base b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/artifacts.html.svn-base
new file mode 100644
index 0000000..0b3cfb8
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/artifacts.html.svn-base
@@ -0,0 +1,48 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Artifacts
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Created artifacts</th>
+            <th>Modified artifacts</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, row in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.created}}<br/>
+                {% endfor %}
+              </td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.modified}}<br/>
+                {% endfor %}
+              </td>
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/commits.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/commits.html.svn-base b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/commits.html.svn-base
new file mode 100644
index 0000000..c574c9f
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/commits.html.svn-base
@@ -0,0 +1,37 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Code contribution
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Number of commits</th>
+            <th>Lines of code</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.number}}</td>
+              <td>{{el.lines}}</td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/index.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/index.html.svn-base b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/index.html.svn-base
new file mode 100644
index 0000000..b53e596
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/index.html.svn-base
@@ -0,0 +1,341 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution
+    {% if category %}
+        in projects of category {{category.fullname}}
+    {% endif %}
+{% endblock %}
+
+{% block content %}
+  {% if user %}
+
+    <h2>General statistics</h2>
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Value</th>
+        </tr>
+      </thead>
+      <tbody> 
+        <tr>
+          <td>Registration date</td>
+          <td>
+            {{registration_date.strftime("%d %b %Y, %H:%M:%S (UTC)")}}, 
+            {{days}} day{% if days != 1 %}s{% endif %} ago</td>
+        </tr>
+        {% if last_login %}
+          <tr>
+            <td>Last login</td>
+            <td>
+              {{last_login.strftime("%d %b %Y, %H:%M:%S (UTC)")}},
+              {{last_login_days}} day{% if last_login_days != 1 %}s{% endif %} ago</td>
+            </td>
+          </tr>
+        {% endif %}
+      </tbody>
+    </table>
+
+    <h2>Contribution statistics</h2>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Total value</th>
+          <th>Average per-month value</th>
+          <th>Last 30 days</th>
+          {% if days >= 30 %}
+            <th>Trend</th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        {% if not category %}
+          <tr>
+            <td>Logins</td>
+            <td>{{totlogins}}</td>
+            <td>{{permonthlogins}}</td>
+            <td>{{lastmonth_logins}}</td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_logins > permonthlogins %}
+                  Up
+                {% elif lastmonth_logins == permonthlogins %}
+                  =
+                {% else %}
+                  Down
+                {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endif %}
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Commits number</a></td>
+          <td>{{totcommits.number}}</td>
+          <td>{{permonthcommits.number}}</td>
+          <td>{{lastmonthcommits.number}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if permonthcommits.number > permonthcommits.number %}
+                Up
+              {% elif permonthcommits.number == permonthcommits.number %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Added/modified LOCs</a></td>
+          <td>{{totcommits.lines}}</td>
+          <td>{{permonthcommits.lines}}</td>
+          <td>{{lastmonthcommits.lines}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if permonthcommits.lines > permonthcommits.lines %}
+                Up
+              {% elif permonthcommits.lines == permonthcommits.lines %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/artifacts/">Total number of created artifacts</a></td>
+          <td>{{totartifacts.created}}</td>
+          <td>{{permonthartifacts.created}}</td>
+          <td>{{lastmonthartifacts.created}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthartifacts.created > permonthartifacts.created %}
+                Up
+              {% elif lastmonthartifacts.created == permonthartifacts.created %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/commits/">Total number of edited artifacts</a></td>
+          <td>{{totartifacts.modified}}</td>
+          <td>{{permonthartifacts.modified}}</td>
+          <td>{{lastmonthartifacts.modified}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthartifacts.modified > permonthartifacts.modified %}
+                Up
+              {% elif lastmonthartifacts.modified == permonthartifacts.modified %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        
+        {% for key, value in artifacts_by_type.items() %}
+          <tr>
+            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Created {{key}} artifacts</a></td>
+            <td>{{value.created}}</td>
+            <td>{{value.pmcreated}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].created}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].created > value.pmcreated %}
+                    Up
+                  {% elif lastmonth_artifacts_by_type[key].created == value.pmcreated %}
+                    =
+                  {% else %}
+                    Down
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+          <tr>
+            <td><a href="/userstats/{{user.username}}/metric/artifacts/">Edited {{key}} artifacts</a></td>
+            <td>{{value.modified}}</td>
+            <td>{{value.pmmodified}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].modified}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td>
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].modified > value.pmmodified %}
+                    Up
+                  {% elif lastmonth_artifacts_by_type[key].modified == value.pmmodified %}
+                    =
+                  {% else %}
+                    Down
+                  {%endif%}
+                {%else%} Down {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endfor %}
+
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Assigned tickets</a></td>
+          <td>{{tottickets.assigned}}</td>
+          <td>{{permonthtickets.assigned}}</td>
+          <td>{{lastmonthtickets.assigned}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.assigned > permonthtickets.assigned %}
+                Up
+              {% elif lastmonthtickets.assigned == permonthtickets.assigned %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Revoked tickets</a></td>
+          <td>{{tottickets.revoked}}</td>
+          <td>{{permonthtickets.revoked}}</td>
+          <td>{{lastmonthtickets.revoked}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.revoked > permonthtickets.revoked %}
+                Up
+              {% elif lastmonthtickets.revoked == permonthtickets.revoked %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Solved tickets</a></td>
+          <td>{{tottickets.solved}}</td>
+          <td>{{permonthtickets.solved}}</td>
+          <td>{{lastmonthtickets.solved}}</td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.solved > permonthtickets.solved %}
+                Up
+              {% elif lastmonthtickets.solved == permonthtickets.solved %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td><a href="/userstats/{{user.username}}/metric/tickets/">Average tickets solving time</a></td>
+          <td>
+            {% if tottickets.averagesolvingtime %}
+              {{tottickets.averagesolvingtime.days}} days, 
+              {{tottickets.averagesolvingtime.hours}} hours,
+              {{tottickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          <td>n/a</td>
+          <td>
+            {% if lastmonthtickets.averagesolvingtime %}
+              {{lastmonthtickets.averagesolvingtime.days}} days, 
+              {{lastmonthtickets.averagesolvingtime.hours}} hours,
+              {{lastmonthtickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          {% if days >= 30 %}
+            <td>
+              {% if lastmonthtickets.averagesolvingtime > tottickets.averagesolvingtime %}
+                Up
+              {% elif lastmonthtickets.averagesolvingtime == tottickets.averagesolvingtime %}
+                =
+              {% else %}
+                Down
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+      </tbody>
+    </table>
+
+    {% if categories %}
+        <h2>Prefered categories</h2>
+        <table>
+          <thead>
+            <tr>
+              <th>Category name</th>
+              <th>Number of projects</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for cat, count in categories %}
+              <tr>
+                <td><a href="/userstats/{{user.username}}/category/{{cat.fullname}}">{{cat.fullname}}</a></td>
+                <td>{{count}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+    {% endif %}
+    {% if category %}
+        <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+    {% else %}
+      <h2>Overall evaluation</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Field</th>
+            <th>Evaluation</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>Code contribution</td>
+            <td>
+               {% for i in range(codestars) %}★{% endfor %}
+               {% for i in range(5 - codestars) %}☆{% endfor %}
+            </td>
+          </tr>
+          <tr>
+            <td>Contribution to discussions on the forge</td>
+            <td>
+               {% for i in range(discussionstars) %}★{% endfor %}
+               {% for i in range(5 - discussionstars) %}☆{% endfor %}
+            </td>
+          </tr>
+          <tr>
+            <td>Contribution to issues solving</td>
+            <td>
+              {% for i in range(ticketsstars) %}★{% endfor %}
+              {% for i in range(5 - ticketsstars) %}☆{% endfor %}
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+  {% else %}
+    Invalid user!
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/tickets.html.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/tickets.html.svn-base b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/tickets.html.svn-base
new file mode 100644
index 0000000..148cfa8
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/.svn/text-base/tickets.html.svn-base
@@ -0,0 +1,47 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}User stats{% endblock %}
+
+{% block header %}
+    Statistics about {{user.display_name}}'s contribution – Tickets
+{% endblock %}
+
+{% block content %}
+
+  {% if user %}
+
+    {% if data %}
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Assigned tickets</th>
+            <th>Solved tickets</th>
+            <th>Revoked tickets</th>
+            <th>Average solving time</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.assigned}}</td>
+              <td>{{el.solved}}</td>
+              <td>{{el.revoked}}</td>
+              <td>
+                {% if el.averagesolvingtime %}
+                  {{el.averagesolvingtime.days}} days, 
+                  {{el.averagesolvingtime.hours}} hours,
+                  {{el.averagesolvingtime.minutes}} min
+                {% else %}n/a{% endif %}
+              </td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    {% endif %}
+    <div class="grid-20"><a href="/userstats/{{user.username}}">Go back to general statistics</a></div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/artifacts.html
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/artifacts.html b/ForgeOrganizationStats/forgeorganizationstats/templates/artifacts.html
new file mode 100644
index 0000000..d3edf6f
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/artifacts.html
@@ -0,0 +1,67 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Organization stats{% endblock %}
+
+{% block header %}
+    Statistics about {{organization.fullname}}'s contribution – Artifacts
+{% endblock %}
+
+{% block content %}
+
+  {% if organization and (organization.stats.visible or (c.user.username in c.project.admins())) %}
+    <div class="grid-20">
+      <ul>
+        <li><a href="{{c.project.url()}}organizationstats">Go back to general statistics</a></li>
+      </ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Created artifacts</th>
+            <th>Modified artifacts</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, row in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.created}}<br/>
+                {% endfor %}
+              </td>
+              <td>
+                {% for details in row %}
+                  {% if details.messagetype %} {{details.messagetype}}:
+                  {% else %}Total:{% endif %} {{details.modified}}<br/>
+                {% endfor %}
+              </td>
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+  {% else %}
+    {% if not organization %}
+      <h2>Invalid organization</h2>
+      <div class="grid-20"> 
+        You are looking for the statistics of an organization which doesn't exist on this forge. Check your url.
+      </div>
+    {% else %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        The administrator of this organization has set the preferences so that statistics are not visible
+        to other users of the forge.
+      </div>
+    {% endif %}
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/commits.html
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/commits.html b/ForgeOrganizationStats/forgeorganizationstats/templates/commits.html
new file mode 100644
index 0000000..8368e45
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/commits.html
@@ -0,0 +1,56 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Organization stats{% endblock %}
+
+{% block header %}
+    Statistics about {{organization.fullname}}'s contribution – Code contribution
+{% endblock %}
+
+{% block content %}
+
+  {% if organization and (organization.stats.visible or (c.user.username in c.project.admins())) %}
+    <div class="grid-20">
+      <ul>
+        <li><a href="{{c.project.url()}}organizationstats">Go back to general statistics</a></li>
+      </ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Number of commits</th>
+            <th>Lines of code</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.number}}</td>
+              <td>{{el.lines}}</td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+  {% else %}
+    {% if not organization %}
+      <h2>Invalid organization</h2>
+      <div class="grid-20"> 
+        You are looking for the statistics of an organization which doesn't exist on this forge. Check your url.
+      </div>
+    {% else %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        The administrator of this organization has set the preferences so that statistics are not visible
+        to other users of the forge.
+      </div>
+    {% endif %}
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/index.html b/ForgeOrganizationStats/forgeorganizationstats/templates/index.html
new file mode 100644
index 0000000..087ac58
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/index.html
@@ -0,0 +1,509 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Organization stats{% endblock %}
+
+{% block header %}
+    Statistics about {{organization.fullname}}'s contribution
+    {% if category %}
+        in projects of category {{category.fullname}}
+    {% endif %}
+{% endblock %}
+
+{% block content %}
+  {% if organization and (organization.stats.visible or (c.user.username in c.project.admins())) %}
+
+    {% if category %}
+      <ul>
+        <li><a href="{{c.project.url()}}organizationstats">Go back to general statistics</a></li>
+      </ul>
+    {% endif %}
+
+    <h2>General statistics</h2>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Date</th>
+          <th>Time interval</th>
+        </tr>
+      </thead>
+      <tbody> 
+        <tr>
+          <td>Registration date</td>
+          <td>{{registration_date.strftime("%d %b %Y, %H:%M:%S (UTC)")}}</td> 
+           <td>{{days}} day{% if days != 1 %}s{% endif %} ago</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <h2>Contribution statistics</h2>
+
+    <table>
+      <thead>
+        <tr>
+          <th>Parameter</th>
+          <th>Total value</th>
+          <th>Average per-month value</th>
+          <th>Last 30 days</th>
+          {% if days >= 30 %}
+            <th>Trend</th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>
+            {%if totcommits.number > 0 %}
+              <a href="{{c.project.url()}}organizationstats/commits/">Commits number</a>
+            {% else %}
+              Commits number
+            {% endif %}
+          </td>
+          <td>{{totcommits.number}}</td>
+          <td>{{permonthcommits.number}}</td>
+          <td>{{lastmonthcommits.number}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthcommits.number > permonthcommits.number %}
+                  <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthcommits.number == permonthcommits.number %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if totcommits.lines > 0 %}
+              <a href="{{c.project.url()}}organizationstats/commits/">Added/modified LOCs</a>
+            {% else %}
+              Added/modified LOCs
+            {% endif %}
+          </td>
+          <td>{{totcommits.lines}}</td>
+          <td>{{permonthcommits.lines}}</td>
+          <td>{{lastmonthcommits.lines}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthcommits.lines > permonthcommits.lines %}
+                  <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthcommits.lines == permonthcommits.lines %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if totartifacts.created > 0 %}
+              <a href="{{c.project.url()}}organizationstats/artifacts/">Total number of created artifacts</a>
+            {% else %}
+              Total number of created artifacts
+            {% endif %}
+          </td>
+          <td>{{totartifacts.created}}</td>
+          <td>{{permonthartifacts.created}}</td>
+          <td>{{lastmonthartifacts.created}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthartifacts.created > permonthartifacts.created %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthartifacts.created == permonthartifacts.created %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if totartifacts.modified > 0 %}
+              <a href="{{c.project.url()}}organizationstats/artifacts/">Total number of edited artifacts</a>
+            {% else %}
+              Total number of edited artifacts
+            {% endif %}
+          </td>
+          <td>{{totartifacts.modified}}</td>
+          <td>{{permonthartifacts.modified}}</td>
+          <td>{{lastmonthartifacts.modified}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthartifacts.modified > permonthartifacts.modified %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthartifacts.modified == permonthartifacts.modified %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        
+        {% for key, value in artifacts_by_type.items() %}
+          <tr>
+            <td>
+              {%if value.created > 0 %}
+                <a href="{{c.project.url()}}organizationstats/artifacts/">Created {{key}} artifacts</a>
+              {% else %}
+                Created {{key}} artifacts
+              {% endif %}
+            </td>
+            <td>{{value.created}}</td>
+            <td>{{value.pmcreated}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].created}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td style="text-align:center;">
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].created > value.pmcreated %}
+                    <img src="{{g.forge_static('images/up.png')}}"/>
+                  {% elif lastmonth_artifacts_by_type[key].created == value.pmcreated %}
+                    <img src="{{g.forge_static('images/equal.png')}}"/>
+                  {% else %}
+                    <img src="{{g.forge_static('images/down.png')}}"/>
+                  {%endif%}
+                {%else%} <img src="{{g.forge_static('images/down.png')}}"/> {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+          <tr>
+            <td>
+              {%if value.modified > 0 %}
+                <a href="{{c.project.url()}}organizationstats/artifacts/">Edited {{key}} artifacts</a>
+              {% else %}
+                Edited {{key}} artifacts
+              {% endif %}
+            </td>
+            <td>{{value.modified}}</td>
+            <td>{{value.pmmodified}}</td>
+            <td>
+              {% if lastmonth_artifacts_by_type.get(key) %}
+                 {{lastmonth_artifacts_by_type[key].modified}}
+              {% else %}
+                 0
+              {% endif %}
+            </td>
+            {% if days >= 30 %}
+              <td style="text-align:center;">
+                {% if lastmonth_artifacts_by_type.get(key) %}
+                  {% if lastmonth_artifacts_by_type[key].modified > value.pmmodified %}
+                    <img src="{{g.forge_static('images/up.png')}}"/>
+                  {% elif lastmonth_artifacts_by_type[key].modified == value.pmmodified %}
+                    <img src="{{g.forge_static('images/equal.png')}}"/>
+                  {% else %}
+                    <img src="{{g.forge_static('images/down.png')}}"/>
+                  {%endif%}
+                {%else%} <img src="{{g.forge_static('images/down.png')}}"/> {%endif%}
+              </td>
+            {% endif %}
+          </tr>
+        {% endfor %}
+
+        <tr>
+          <td>
+            {%if tottickets.assigned > 0 %}
+              <a href="{{c.project.url()}}organizationstats/tickets/">Assigned tickets</a>
+            {% else %}
+              Assigned tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.assigned}}</td>
+          <td>{{permonthtickets.assigned}}</td>
+          <td>{{lastmonthtickets.assigned}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.assigned > permonthtickets.assigned %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.assigned == permonthtickets.assigned %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if tottickets.revoked > 0 %}
+              <a href="{{c.project.url()}}organizationstats/tickets/">Revoked tickets</a>
+            {% else %}
+              Revoked tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.revoked}}</td>
+          <td>{{permonthtickets.revoked}}</td>
+          <td>{{lastmonthtickets.revoked}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.revoked > permonthtickets.revoked %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.revoked == permonthtickets.revoked %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if tottickets.solved > 0 %}
+              <a href="{{c.project.url()}}organizationstats/tickets/">Solved tickets</a>
+            {% else %}
+              Solved tickets
+            {% endif %}
+          </td>
+          <td>{{tottickets.solved}}</td>
+          <td>{{permonthtickets.solved}}</td>
+          <td>{{lastmonthtickets.solved}}</td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.solved > permonthtickets.solved %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.solved == permonthtickets.solved %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+        <tr>
+          <td>
+            {%if tottickets.averagesolvingtime > 0 %}
+              <a href="{{c.project.url()}}organizationstats/tickets/">Average tickets solving time</a>
+            {% else %}
+              Average tickets solving time
+            {% endif %}
+          </td>
+          <td>
+            {% if tottickets.averagesolvingtime %}
+              {{tottickets.averagesolvingtime.days}} days, 
+              {{tottickets.averagesolvingtime.hours}} hours,
+              {{tottickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          <td>n/a</td>
+          <td>
+            {% if lastmonthtickets.averagesolvingtime %}
+              {{lastmonthtickets.averagesolvingtime.days}} days, 
+              {{lastmonthtickets.averagesolvingtime.hours}} hours,
+              {{lastmonthtickets.averagesolvingtime.minutes}} min
+            {% else %}n/a{% endif %}
+          </td>
+          {% if days >= 30 %}
+            <td style="text-align:center;">
+              {% if lastmonthtickets.averagesolvingtime > tottickets.averagesolvingtime %}
+                <img src="{{g.forge_static('images/up.png')}}"/>
+              {% elif lastmonthtickets.averagesolvingtime == tottickets.averagesolvingtime %}
+                <img src="{{g.forge_static('images/equal.png')}}"/>
+              {% else %}
+                <img src="{{g.forge_static('images/down.png')}}"/>
+              {%endif%}
+            </td>
+          {% endif %}
+        </tr>
+      </tbody>
+    </table>
+
+    {% if not category %}
+      <h2>Members</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Parameter</th>
+            <th>Value</th>
+          </tr>
+        </thead>
+        <tbody> 
+          <tr>
+            <td>Current number of registered members</td>
+            <td>{{membersnumber}}</td> 
+          </tr>
+          <tr>
+            <td>Members joining the organization during the last 30 days</td>
+            <td>{{newmembers}}</td> 
+          </tr>
+          <tr>
+            <td>Members leaving the organization during the last 30 days</td>
+            <td>{{leftmembers}}</td> 
+          </tr>
+        </tbody>
+      </table>
+
+      <h2>Per-member contributions</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Parameter</th>
+            <th>Last 30 days</th>
+          </tr>
+        </thead>
+        <tbody> 
+          <tr>
+            <td>Created artifacts</td>
+            <td>{{permemberartifacts.created}}</td> 
+          </tr>
+          <tr>
+            <td>Modified artifacts</td>
+            <td>{{permemberartifacts.modified}}</td> 
+          </tr>
+          <tr>
+            <td>Number of commits</td>
+            <td>{{permembercommits.number}}</td> 
+          </tr>
+          <tr>
+            <td>Number of committed lines of code</td>
+            <td>{{permembercommits.lines}}</td> 
+          </tr>
+          <tr>
+            <td>Number of assigned tickets</td>
+            <td>{{permembertickets.assigned}}</td> 
+          </tr>
+          <tr>
+            <td>Number of solved tickets</td>
+            <td>{{permembertickets.solved}}</td> 
+          </tr>
+          <tr>
+            <td>Number of revoked tickets</td>
+            <td>{{permembertickets.revoked}}</td> 
+          </tr>
+        </tbody>
+      </table>
+
+      <h2>Projects</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Type</th>
+            <th>Current number</th>
+            <th>Joined in the last month</th>
+            <th>Left in the last month</th>
+          </tr>
+        </thead>
+        <tbody> 
+          <tr>
+            <td>Cooperations</td>
+            <td>{{coopnumber}}</td> 
+            <td>{{newcooperations}}</td> 
+            <td>{{oldcooperations}}</td> 
+          </tr>
+          <tr>
+            <td>Participations</td>
+            <td>{{participnumber}}</td> 
+            <td>{{newparticipations}}</td> 
+            <td>{{oldparticipations}}</td> 
+          </tr>
+        </tbody>
+      </table>
+
+      {% if categories %}
+          <h2>Preferred categories</h2>
+          <p>
+            The following table shows the number projects tagged as belonging to each single category in which this organization is 
+            currently involved.
+          </p>
+          <table>
+            <thead>
+              <tr>
+                <th>Category name</th>
+                <th>Number of projects</th>
+              </tr>
+            </thead>
+            <tbody>
+              {% for cat, count in categories %}
+                <tr>
+                  <td><a href="{{c.project.url()}}organizationstats/category/{{cat.shortname}}">{{cat.fullname}}</a></td>
+                  <td>{{count}}</td>
+                </tr>
+              {% endfor %}
+            </tbody>
+          </table>
+          
+          {% if categories|length > 1 %}
+            <p>
+              The same data listed in the previous table is graphically presented by the following histogram.
+            </p>
+            <p>
+              <img src="{{c.project.url()}}organizationstats/categories_graph"/>
+            </p>
+          {% endif %}
+      {% endif %}
+      {% if not category %}
+        <h2>Overall evaluation</h2>
+        <table>
+          <thead>
+            <tr>
+              <th>Field</th>
+              <th>Value</th>
+              <th>Average</th>
+              <th>Maximum</th>
+              <th>Progressbar</th>
+              <th>Percentage</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>Code</td>
+              <td>{{codecontribution}} LOC{% if codecontribution != 1 %}s{% endif %}/month</td>
+              <td>{{averagecodecontrib}} LOC{% if averagecodecontrib != 1 %}s{% endif %}/month</td>
+              <td>{{maxcodecontrib}} LOC{% if maxcodecontrib != 1 %}s{% endif %}/month</td>
+              <td><img src="{{c.project.url()}}organizationstats/code_ranking_bar"/></td>
+              <td>{{codepercentage}} %</td>
+            </tr>
+            <tr>
+              <td>Discussion</td>
+              <td>{{discussioncontribution}} contribution{% if discussioncontribution != 1 %}s{% endif %}/month</td>
+              <td>{{averagedisccontrib}} contribution{% if averagedisccontrib != 1 %}s{% endif %}/month</td>
+              <td>{{maxdisccontrib}} contribution{% if maxdisccontrib != 1 %}s{% endif %}/month</td>
+              <td><img src="{{c.project.url()}}organizationstats/discussion_ranking_bar"/></td>
+              <td>{{discussionpercentage}} %</td>
+            </tr>
+            <tr>
+               <td>Solved issues</td>
+               <td>{{ticketcontribution}} %</td>
+               <td>{{averageticketcontrib}} %</td>
+              <td>{{maxticketcontrib}} %</td>
+              <td><img src="{{c.project.url()}}organizationstats/tickets_ranking_bar"/></td>
+              <td>{{ticketspercentage}} %</td>
+            </tr>
+          </tbody>
+        </table>
+        <h3>Note</h3>
+        <p>
+           The above table compares the average monthly contribution of this organization with the average monthly contributions of 
+           the other organization of the forge. The progressbar and the percentage refer to the organization's position in an overall
+           ranking of the organization of this forge. For example, a value of 100% in the field "Code" is associated to the 
+           organization whose users have generate the highest total average number of committed LOCs per month. 
+           Of course, this doesn't consider the quality of the contributions.
+        </p>
+      {% endif %}
+    {% endif %}
+  {% else %}
+    {% if not organization %}
+      <h2>Invalid organization</h2>
+      <div class="grid-20"> 
+        You are looking for the statistics of an organization which doesn't exist on this forge. Check your url.
+      </div>
+    {% else %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        The administrator of this organization has set the preferences so that statistics are not visible
+        to other users of the forge.
+      </div>
+    {% endif %}
+  {% endif %}
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/settings.html
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/settings.html b/ForgeOrganizationStats/forgeorganizationstats/templates/settings.html
new file mode 100644
index 0000000..8f4f532
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/settings.html
@@ -0,0 +1,19 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Organization stats – Settings{% endblock %}
+
+{% block header %}
+    Statistics about {{organization.fullname}}'s contribution – Settings
+{% endblock %}
+
+{% block content %}
+
+    <div class="grid-20">
+      In this page you can set the visibility of the statistics related to your organization. If you decide to hide your 
+      organization's statistics to other users, data collected about contributions of your organization to projects hosted on this 
+      forge will be available only to users with admin access to the profile of your organization. 
+      {{form.display(organization = organization)}}
+    </div>
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/templates/tickets.html
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/templates/tickets.html b/ForgeOrganizationStats/forgeorganizationstats/templates/tickets.html
new file mode 100644
index 0000000..8f3f050
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/templates/tickets.html
@@ -0,0 +1,66 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Organization stats{% endblock %}
+
+{% block header %}
+    Statistics about {{organization.fullname}}'s contribution – Tickets
+{% endblock %}
+
+{% block content %}
+
+  {% if organization and (organization.stats.visible or (c.user.username in c.project.admins())) %}
+    <div class="grid-20">
+      <ul>
+        <li><a href="{{c.project.url()}}organizationstats">Go back to general statistics</a></li>
+      </ul>
+    </div>
+
+    {% if data %}
+    <div class="grid-20">
+      <h2>Statistics by category</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Category</th>
+            <th>Assigned tickets</th>
+            <th>Solved tickets</th>
+            <th>Revoked tickets</th>
+            <th>Average solving time</th>
+          </tr>
+        </thead>
+        <tbody> 
+          {% for cat, el in data.items() %}
+            <tr>
+              <td>{% if cat %}{{cat.fullname}}{% else %}All categories{% endif %}</td>
+              <td>{{el.assigned}}</td>
+              <td>{{el.solved}}</td>
+              <td>{{el.revoked}}</td>
+              <td>
+                {% if el.averagesolvingtime %}
+                  {{el.averagesolvingtime.days}} days, 
+                  {{el.averagesolvingtime.hours}} hours,
+                  {{el.averagesolvingtime.minutes}} min
+                {% else %}n/a{% endif %}
+              </td>
+            {% endfor %}
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    {% endif %}
+  {% else %}
+    {% if not organization %}
+      <h2>Invalid organization</h2>
+      <div class="grid-20"> 
+        You are looking for the statistics of an organization which doesn't exist on this forge. Check your url.
+      </div>
+    {% else %}
+      <h2>Statistics not available</h2>
+      <div class="grid-20"> 
+        The administrator of this organization has set the preferences so that statistics are not visible
+        to other users of the forge.
+      </div>
+    {% endif %}
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/tests/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/tests/__init__.py b/ForgeOrganizationStats/forgeorganizationstats/tests/__init__.py
new file mode 100644
index 0000000..e69de29