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:23 UTC

[1/5] organization and organization stats

Updated Branches:
  refs/heads/si/5566 [created] aa1a90dbf


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/tests/test_model.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/tests/test_model.py b/ForgeOrganizationStats/forgeorganizationstats/tests/test_model.py
new file mode 100644
index 0000000..980ef7e
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/tests/test_model.py
@@ -0,0 +1,396 @@
+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 forgeorganizationstats.model import OrganizationStats
+from forgeorganization.organization.model import Organization, Membership, ProjectInvolvement
+
+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.user1 = User.register(dict(username='test-new-user-1',
+            display_name='Test Stats 1'),
+            make_project=False)
+        c.user = self.user1
+        self.organization = Organization.register(
+            'testorg', 'Test Organization', 'For-profit business', self.user1)
+        self.user2 = User.register(dict(username='test-new-user-2',
+            display_name='Test Stats 2'),
+            make_project=False)
+        self.m1 = Membership.insert('Developer', 'active', 
+            self.organization._id, self.user1._id)
+        self.m2 = Membership.insert('Developer', 'active', 
+            self.organization._id, self.user2._id)
+        self.organization.project().add_user(self.user1, ['Admin'])
+        self.organization.project().add_user(self.user2, ['Admin'])
+
+        self.project = M.Project.query.get(shortname='test')
+        self.project.add_user(self.user1, ['Admin'])
+        self.project.add_user(self.user2, ['Admin'])
+        pi = ProjectInvolvement.insert('active', 'cooperation', 
+            self.organization._id, self.project._id)
+        c.user = self.user1
+
+    def test_init_values(self):
+        artifacts = self.organization.stats.getArtifacts()
+        tickets = self.organization.stats.getTickets()
+        commits = self.organization.stats.getCommits()
+        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.organization.stats.getLastMonthArtifacts()
+        lmtickets = self.organization.stats.getLastMonthTickets()
+        lmcommits = self.organization.stats.getLastMonthCommits()
+        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_create_artifact_stats(self):
+        p = self.project
+        topic = TroveCategory.query.get(shortname='scientific')
+
+        init_lm_art = self.organization.stats.getLastMonthArtifacts()
+        init_art = self.organization.stats.getArtifacts()
+        init_art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        init_art_by_type = self.organization.stats.getArtifactsByType()
+        init_lm_art_by_type = self.organization.stats.getLastMonthArtifactsByType()
+        init_art_sci = self.organization.stats.getArtifacts(category=topic._id)
+
+        if not init_art_by_type.get('Wiki'):
+            init_art_by_type['Wiki'] = {'created':0, 'modified':0}
+        if not init_lm_art_by_type.get('Wiki'):
+            init_lm_art_by_type['Wiki'] = {'created':0, 'modified':0}
+        self.organization.stats.addNewArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.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.organization.stats.addNewArtifact('Wiki', new_date, p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.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.organization.stats.addNewArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.stats.getLastMonthArtifactsByType()
+        art_sci = self.organization.stats.getArtifacts(category=topic._id)
+        art_by_cat = self.organization.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.organization.stats.getArtifactsByCategory(detailed=False)
+        assert art_by_cat[topic]['created'] == 1 and art_by_cat[topic]['modified'] == 0
+
+        lm_per_member = self.organization.stats.getLastMonthArtifactsPerMember()
+        assert lm_per_member['created'] == 1.0
+        assert lm_per_member['modified'] == 0.0
+
+    def test_modify_artifact_stats(self):
+        p = self.project
+        topic = TroveCategory.query.get(shortname='scientific')
+
+        init_lm_art = self.organization.stats.getLastMonthArtifacts()
+        init_art = self.organization.stats.getArtifacts()
+        init_art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        init_art_by_type = self.organization.stats.getArtifactsByType()
+        init_lm_art_by_type = self.organization.stats.getLastMonthArtifactsByType()
+        init_art_sci = self.organization.stats.getArtifacts(category=topic._id)
+        if not init_art_by_type.get('Wiki'):
+            init_art_by_type['Wiki'] = {'created':0, 'modified':0}
+        if not init_lm_art_by_type.get('Wiki'):
+            init_lm_art_by_type['Wiki'] = {'created':0, 'modified':0}
+
+        self.organization.stats.addModifiedArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.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.organization.stats.addModifiedArtifact('Wiki', new_date, p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.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.organization.stats.addModifiedArtifact('Wiki', datetime.utcnow(), p)
+        lm_art = self.organization.stats.getLastMonthArtifacts()
+        artifacts = self.organization.stats.getArtifacts()
+        art_wiki = self.organization.stats.getArtifacts(art_type='Wiki')
+        art_by_type = self.organization.stats.getArtifactsByType()
+        lm_art_by_type = self.organization.stats.getLastMonthArtifactsByType()
+        art_sci = self.organization.stats.getArtifacts(category=topic._id)
+        art_by_cat = self.organization.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.organization.stats.getArtifactsByCategory(detailed=False)
+        assert art_by_cat[topic]['created'] == 0 and art_by_cat[topic]['modified'] == 1
+
+        lm_per_member = self.organization.stats.getLastMonthArtifactsPerMember()
+        assert lm_per_member['created'] == 0.0
+        assert lm_per_member['modified'] == 1.0
+
+    def test_ticket_stats(self):
+        p = self.project
+        topic = TroveCategory.query.get(shortname='scientific')
+        create_time = datetime.utcnow() + timedelta(-5)
+
+        init_lm_tickets_art = self.organization.stats.getLastMonthArtifacts(art_type='Ticket')
+        init_tickets_art = self.organization.stats.getArtifacts(art_type='Ticket')
+        init_tickets_sci_art = self.organization.stats.getArtifacts(category=topic._id)
+        init_tickets = self.organization.stats.getTickets()
+        init_lm_tickets = self.organization.stats.getLastMonthTickets()
+
+        self.organization.stats.addNewArtifact('Ticket', create_time, p)
+        lm_tickets_art = self.organization.stats.getLastMonthArtifacts(art_type='Ticket')
+        tickets_art = self.organization.stats.getArtifacts(art_type='Ticket')
+        tickets_sci_art = self.organization.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.organization.stats.addAssignedTicket(create_time, p)
+        tickets = self.organization.stats.getTickets()
+        lm_tickets = self.organization.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.organization.stats.addRevokedTicket(create_time + timedelta(-32), p)
+        tickets = self.organization.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.organization.stats.addClosedTicket(create_time, create_time + timedelta(1), p)
+        tickets = self.organization.stats.getTickets()
+        lm_tickets = self.organization.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.organization.stats.addClosedTicket(create_time, create_time + timedelta(3), p)
+        tickets = self.organization.stats.getTickets()
+        lm_tickets = self.organization.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.organization.stats.getTicketsByCategory()
+        lm_by_cat = self.organization.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
+
+        lm_per_member = self.organization.stats.getLastMonthTicketsPerMember()
+        assert lm_per_member['assigned'] == 0.5
+        assert lm_per_member['solved'] == 1.0
+        assert lm_per_member['revoked'] == 0.0
+
+    @with_git
+    def test_commit_stats(self):
+        p = self.project
+        topic = TroveCategory.query.get(shortname='scientific')
+        commit_time = datetime.utcnow() + timedelta(-1)
+
+        self.user1.set_password('testpassword')
+        addr = M.EmailAddress.upsert('rcopeland@geek.net')
+        self.user1.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.organization.stats.getCommits()
+        assert init_commits['number'] == 4
+        init_lmcommits = self.organization.stats.getLastMonthCommits()
+        assert init_lmcommits['number'] == 4
+ 
+        lm_per_member = self.organization.stats.getLastMonthCommitsPerMember()
+        assert lm_per_member['number'] == 2.0
+
+        p.trove_topic = [topic._id]
+        self.organization.stats.addCommit(commit, datetime.utcnow(), p)
+        commits = self.organization.stats.getCommits()
+        assert commits['number'] == init_commits['number'] + 1
+        assert commits['lines'] == init_commits['lines'] + 1
+        lmcommits = self.organization.stats.getLastMonthCommits()
+        assert lmcommits['number'] == init_lmcommits['number'] + 1
+        assert lmcommits['lines'] == init_lmcommits['lines'] + 1
+        by_cat = self.organization.stats.getCommitsByCategory()
+        assert by_cat[topic]['number'] == 1
+        assert by_cat[topic]['lines'] == 1
+        lm_by_cat = self.organization.stats.getLastMonthCommitsByCategory()
+        assert lm_by_cat[topic]['number'] == 1
+        assert lm_by_cat[topic]['lines'] == 1
+
+        self.organization.stats.addCommit(commit, datetime.utcnow() + timedelta(-40), p)
+        commits = self.organization.stats.getCommits()
+        assert commits['number'] == init_commits['number'] + 2
+        assert commits['lines'] == init_commits['lines'] + 2
+        lmcommits = self.organization.stats.getLastMonthCommits()
+        assert lmcommits['number'] == init_lmcommits['number'] + 1
+        assert lmcommits['lines'] == init_lmcommits['lines'] + 1
+        by_cat = self.organization.stats.getCommitsByCategory()
+        assert by_cat[topic]['number'] == 2
+        assert by_cat[topic]['lines'] == 2
+        lm_by_cat = self.organization.stats.getLastMonthCommitsByCategory()
+        assert lm_by_cat[topic]['number'] == 1
+        assert lm_by_cat[topic]['lines'] == 1
+
+
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/tests/test_stats.py b/ForgeOrganizationStats/forgeorganizationstats/tests/test_stats.py
new file mode 100644
index 0000000..a6c10f5
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/tests/test_stats.py
@@ -0,0 +1,283 @@
+import pkg_resources
+import unittest
+
+from pylons import app_globals as g
+from pylons import tmpl_context as c
+
+from alluratest.controller import TestController, setup_basic_test, setup_global_objects
+from allura.tests import decorators as td
+from allura.lib import helpers as h
+from allura import model as M
+
+from forgegit.tests import with_git
+from forgewiki import model as WM
+from forgetracker import model as TM
+from forgeorganization.organization.model import Organization, Membership, ProjectInvolvement
+
+from ming.orm.ormsession import ThreadLocalORMSession
+
+class TestStats(TestController):
+
+    def setUp(self):
+        super(TestStats, self).setUp()
+        self.user1 = M.User.by_username('test-user')
+        self.user2 = M.User.by_username('test-admin')
+        self.user3 = M.User.by_username('test-user-1')
+        self.organization = Organization.register(
+            'testorg', 'Test Organization', 'For-profit business', self.user1)
+        self.organization.project().add_user(self.user1, ['Admin'])
+        self.organization.project().add_user(self.user2, ['Admin'])
+
+        self.m1 = Membership.insert('Developer', 'closed', 
+            self.organization._id, self.user1._id)
+        self.m2 = Membership.insert('Developer', 'active', 
+            self.organization._id, self.user2._id)
+        self.m3 = Membership.insert('Developer', 'active', 
+            self.organization._id, self.user3._id)
+
+        self.project = M.Project.query.get(shortname='test')
+        self.project.add_user(self.user1, ['Admin'])
+        self.project.add_user(self.user2, ['Admin'])
+        self.project.add_user(self.user3, ['Admin'])
+        pi = ProjectInvolvement.insert('active', 'cooperation', 
+            self.organization._id, self.project._id)
+
+    @td.with_tool('test', 'wiki', mount_point='wiki', mount_label='wiki', username='test-admin')
+    def test_wiki_stats(self):
+
+        initial_artifacts = self.organization.stats.getArtifacts()
+        initial_wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        #Try to create a new page as a user enrolled in the organization which
+        #is developing the project
+        self.app.post('/wiki/newtestpage/update', 
+            params=dict(title='newtestpage', text='footext'),
+            extra_environ=dict(username=str(self.user2.username)))
+
+        artifacts = self.organization.stats.getArtifacts()
+        wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 1 + initial_artifacts['created']
+        assert artifacts['modified'] == initial_artifacts['modified']
+        assert wiki['created'] == 1 + initial_wiki['created']
+        assert wiki['modified'] == initial_wiki['modified']
+
+        #Try to create a new page as another user enrolled in the organization
+        #which is developing the project
+        self.app.post('/wiki/newtestpage2/update', 
+            params=dict(title='newtestpage2', text='footext2'),
+            extra_environ=dict(username=str(self.user3.username)))
+
+        artifacts = self.organization.stats.getArtifacts()
+        wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == initial_wiki['modified']
+
+        #Try to edit a page as a user enrolled in the organization which
+        #is developing the project
+        self.app.post('/wiki/newtestpage2/update', 
+            params=dict(title='newtestpage2', text='newcontent'),
+            extra_environ=dict(username=str(self.user2.username)))
+
+        artifacts = self.organization.stats.getArtifacts()
+        wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == 1 + initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == 1 + initial_wiki['modified']
+
+        #Try to create a new page as a user whose enrollment within the 
+        #organization has been marked as closed
+        self.app.post('/wiki/newtestpage3/update', 
+            params=dict(title='newtestpage3', text='footext'),
+            extra_environ=dict(username=str(self.user1.username)))
+
+        artifacts = self.organization.stats.getArtifacts()
+        wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == 1 + initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == 1 + initial_wiki['modified']
+
+        #Try to edit an existing page as a user whose enrollment within the 
+        #organization has been marked as closed
+        self.app.post('/wiki/newtestpage/update', 
+            params=dict(title='newtestpage', text='footext2'),
+            extra_environ=dict(username=str(self.user1.username)))
+
+        artifacts = self.organization.stats.getArtifacts()
+        wiki = self.organization.stats.getArtifacts(art_type="Wiki")
+
+        assert artifacts['created'] == 2 + initial_artifacts['created']
+        assert artifacts['modified'] == 1 + initial_artifacts['modified']
+        assert wiki['created'] == 2 + initial_wiki['created']
+        assert wiki['modified'] == 1 + initial_wiki['modified']
+
+    @td.with_tool('test', 'tickets', mount_point='tickets', mount_label='tickets', username='test-admin')
+    def test_tracker_stats(self):
+
+        initial_tickets = self.organization.stats.getTickets()
+        initial_tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        r = self.app.post('/tickets/save_ticket', 
+            params={'ticket_form.summary':'footext2',
+                    'ticket_form.assigned_to' : str(self.user2.username)},
+            extra_environ=dict(username=str(self.user2.username)))
+
+        tickets = self.organization.stats.getTickets()
+        tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 1
+        assert tickets['solved'] == initial_tickets['solved']
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 1
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified']
+
+        r = self.app.post('/tickets/save_ticket', 
+            params={'ticket_form.summary':'footext3',
+                    'ticket_form.assigned_to' : str(self.user1.username)},
+            extra_environ=dict(username=str(self.user2.username)))
+
+        tickets = self.organization.stats.getTickets()
+        tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 1
+        assert tickets['solved'] == initial_tickets['solved']
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified']
+
+        ticket2num = str(TM.Ticket.query.get(summary='footext3').ticket_num)
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticket2num, 
+            params={'ticket_form.ticket_num' : ticket2num,
+                    'ticket_form.summary':'footext3',
+                    'ticket_form.assigned_to' : str(self.user3.username)},
+            extra_environ=dict(username=str(self.user2.username)))
+
+        tickets = self.organization.stats.getTickets()
+        tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 2
+        assert tickets['solved'] == initial_tickets['solved']
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 1
+ 
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticket2num, 
+            params={'ticket_form.ticket_num' : ticket2num,
+                    'ticket_form.summary':'footext2',
+                    'ticket_form.status':'closed',
+                    'ticket_form.assigned_to' : str(self.user3.username)},
+            extra_environ=dict(username=str(self.user2.username)))
+
+        tickets = self.organization.stats.getTickets()
+        tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 2
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked']
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 2
+
+        ticket1num = str(TM.Ticket.query.get(summary='footext2').ticket_num)
+        r = self.app.post('/tickets/%s/update_ticket_from_widget' % ticket1num, 
+            params={'ticket_form.ticket_num' : ticket1num,
+                    'ticket_form.summary':'footext2',
+                    'ticket_form.status':'closed',
+                    'ticket_form.assigned_to' : str(self.user1.username)},
+            extra_environ=dict(username=str(self.user2.username)))
+
+        tickets = self.organization.stats.getTickets()
+        tickets_artifacts = self.organization.stats.getArtifacts(art_type="Ticket")
+
+        assert tickets['assigned'] == initial_tickets['assigned'] + 2
+        assert tickets['solved'] == initial_tickets['solved'] + 1
+        assert tickets['revoked'] == initial_tickets['revoked'] + 1
+        assert tickets_artifacts['created'] == initial_tickets_artifacts['created'] + 2
+        assert tickets_artifacts['modified'] == initial_tickets_artifacts['modified'] + 3
+
+class TestGitCommitActiveMember(unittest.TestCase, TestController):
+
+    def setUp(self):
+        setup_basic_test()
+        self.user = M.User.by_username('test-admin')
+        self.organization = Organization.register(
+            'testorg', 'Test Organization', 'For-profit business', self.user)
+        self.organization.project().add_user(self.user, ['Admin'])
+
+        self.m = Membership.insert('Developer', 'active', 
+            self.organization._id, self.user._id)
+        
+        self.project = M.Project.query.get(shortname='test')
+        self.project.add_user(self.user, ['Admin'])
+        pi = ProjectInvolvement.insert('active', 'cooperation', 
+            self.organization._id, self.project._id)
+        addr = M.EmailAddress.upsert('rcopeland@geek.net')
+        self.user.claim_address('rcopeland@geek.net')
+        self.setup_with_tools()
+
+    @with_git
+    @td.with_wiki
+    def setup_with_tools(self):
+        setup_global_objects()
+        h.set_context('test', 'src-git', neighborhood='Projects')
+        repo_dir = pkg_resources.resource_filename(
+            'forgeuserstats', 'tests/data')
+        c.app.repo.fs_path = repo_dir
+        c.app.repo.name = 'testgit.git'
+        self.repo = c.app.repo
+        self.repo.refresh()
+        self.rev = M.repo.Commit.query.get(_id=self.repo.heads[0]['object_id'])
+        self.rev.repo = self.repo
+
+    @td.with_user_project('test-admin')
+    def test_commit_member(self):
+        commits = self.organization.stats.getCommits()
+        assert commits['number'] == 4
+        lmcommits = self.organization.stats.getLastMonthCommits()
+        assert lmcommits['number'] == 4
+
+class TestGitCommitPastMember(unittest.TestCase, TestController):
+
+    def setUp(self):
+        setup_basic_test()
+        self.user = M.User.by_username('test-admin')
+        self.organization = Organization.register(
+            'testorg', 'Test Organization', 'For-profit business', self.user)
+        self.organization.project().add_user(self.user, ['Admin'])
+        self.m = Membership.insert('Developer', 'closed', 
+            self.organization._id, self.user._id)
+        
+        self.project = M.Project.query.get(shortname='test')
+        self.project.add_user(self.user, ['Admin'])
+        pi = ProjectInvolvement.insert('active', 'cooperation', 
+            self.organization._id, self.project._id)
+        addr = M.EmailAddress.upsert('rcopeland@geek.net')
+        self.user.claim_address('rcopeland@geek.net')
+        self.setup_with_tools()
+
+    @with_git
+    @td.with_wiki
+    def setup_with_tools(self):
+        setup_global_objects()
+        h.set_context('test', 'src-git', neighborhood='Projects')
+        repo_dir = pkg_resources.resource_filename(
+            'forgeuserstats', 'tests/data')
+        c.app.repo.fs_path = repo_dir
+        c.app.repo.name = 'testgit.git'
+        self.repo = c.app.repo
+        self.repo.refresh()
+        self.rev = M.repo.Commit.query.get(_id=self.repo.heads[0]['object_id'])
+        self.rev.repo = self.repo
+
+    @td.with_user_project('test-admin')
+    def test_commit_member(self):
+        commits = self.organization.stats.getCommits()
+        assert commits['number'] == 0
+        lmcommits = self.organization.stats.getLastMonthCommits()
+        assert lmcommits['number'] == 0

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/version.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/version.py b/ForgeOrganizationStats/forgeorganizationstats/version.py
new file mode 100644
index 0000000..6514373
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/version.py
@@ -0,0 +1,2 @@
+__version_info__ = (0, 0)
+__version__ = '.'.join(map(str, __version_info__))

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/widgets/forms.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/widgets/forms.py b/ForgeOrganizationStats/forgeorganizationstats/widgets/forms.py
new file mode 100644
index 0000000..e6c3ae1
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/widgets/forms.py
@@ -0,0 +1,22 @@
+from allura.lib import validators as V
+from allura.lib.widgets.forms import ForgeForm
+
+from formencode import validators as fev
+
+import ew as ew_core
+import ew.jinja2_ew as ew
+
+class StatsPreferencesForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        visible = ew.Checkbox(
+            label='Make my personal statistics visible to other users.')
+            
+    def display(self, **kw):
+        if kw.get('organization').stats.visible:
+            self.fields['visible'].attrs = {'checked':'true'}      
+        else:
+            self.fields['visible'].attrs = {}    
+        return super(ForgeForm, self).display(**kw)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/setup.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/setup.py b/ForgeOrganizationStats/setup.py
new file mode 100644
index 0000000..0d1ea08
--- /dev/null
+++ b/ForgeOrganizationStats/setup.py
@@ -0,0 +1,32 @@
+from setuptools import setup, find_packages
+import sys, os
+
+from forgeorganizationstats.version import __version__
+
+setup(name='ForgeOrganizationStats',
+      version=__version__,
+      description="",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='',
+      author_email='',
+      url='',
+      license='',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+          'allura',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [allura]
+      organizationstats=forgeorganizationstats.main:ForgeOrganizationStatsApp
+
+      [allura.stats]
+      organizationstats=forgeorganizationstats.main:OrganizationStatsListener
+      """,
+      )

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/test.ini
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/test.ini b/ForgeOrganizationStats/test.ini
new file mode 100644
index 0000000..9234e46
--- /dev/null
+++ b/ForgeOrganizationStats/test.ini
@@ -0,0 +1,56 @@
+#
+# allura - TurboGears 2 testing environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = true
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5000
+
+[app:main]
+use = config:../Allura/test.ini
+
+[app:main_without_authn]
+use = config:../Allura/test.ini#main_without_authn
+
+[app:main_with_amqp]
+use = config:../Allura/test.ini#main_with_amqp
+
+[loggers]
+keys = root, allura, tool
+
+[handlers]
+keys = test
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = test
+
+[logger_allura]
+level = DEBUG
+handlers =
+qualname = allura
+
+[logger_tool]
+level = DEBUG
+handlers =
+qualname = forgeorganization
+
+[handler_test]
+class = FileHandler
+args = ('test.log',)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
+
+


[2/5] organization and organization stats

Posted by st...@apache.org.
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


[3/5] organization and organization stats

Posted by st...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tests/test_organizations.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tests/test_organizations.py b/ForgeOrganization/forgeorganization/tests/test_organizations.py
new file mode 100644
index 0000000..32bc944
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tests/test_organizations.py
@@ -0,0 +1,396 @@
+import pkg_resources
+import unittest
+
+from pylons import app_globals as g
+from pylons import tmpl_context as c
+from ming.orm.ormsession import ThreadLocalORMSession, session
+
+from alluratest.controller import TestController, setup_basic_test, setup_global_objects
+from allura.tests import decorators as td
+from allura.lib import helpers as h
+from allura.model import User
+from allura import model as M
+
+from forgegit.tests import with_git
+from forgeorganization.organization import model as OM
+import tg
+
+class TestOrganization(TestController):
+
+    def setUp(self):
+        setup_basic_test(config='test.ini')
+        setup_global_objects()
+        super(TestOrganization, self).setUp()
+
+        c.user = User.by_username('test-user-1')
+        #Create a test organization
+        self.name = 'testorg'
+        self.fullname = 'Test Organization'
+        self.role = 'Developer'
+        self.orgtype = 'Foundation or other non-profit organization'
+        r = self.app.post('/organization/save_new/', 
+            params=dict(
+                fullname=self.fullname,
+                shortname=self.name,
+                orgtype=self.orgtype,
+                role=self.role),
+            extra_environ=dict(username='test-user-1'))
+
+        self.org = OM.Organization.query.get(shortname=self.name)
+
+        #Add a new user
+        self.user2 = User.by_username('test-user-2')
+        r = self.app.post((self.org.url()+'admin/organizationprofile/invite_user'),
+            params=dict(
+                username = self.user2.username,
+                role = 'Software Engineer'),
+            extra_environ=dict(username='test-user-1'))
+        m = OM.Membership.query.get(
+            member_id=self.user2._id, 
+            organization_id=self.org._id)
+        r = self.app.post('/organization/change_membership',
+            params=dict(
+                status = 'active',
+                membershipid = str(m._id),
+                requestfrom = 'user',
+                role = 'Software Engineer'),
+            extra_environ=dict(username='test-user-2'))
+
+class TestOrganizationGeneral(TestOrganization):
+
+    @td.with_user_project('test-user-1')
+    def test_registration(self):
+        #Test organization registration
+        r = self.app.get('/organization/',
+            extra_environ=dict(username='test-user-1'))
+        assert 'Test Organization' in r
+        org = OM.Organization.query.get(shortname=self.name)
+        assert self.org.fullname == self.fullname
+        assert self.org.organization_type == self.orgtype
+
+        #Check that the user is the administrator of the org. profile
+        m = OM.Membership.query.get(
+            member_id=c.user._id,
+            organization_id=self.org._id)
+        assert c.user.username in org.project().admins()
+        assert m.status == 'active'
+        assert m.role == self.role
+
+    @td.with_user_project('test-user-1')
+    def test_update_profile(self):
+
+        fullname = 'New Full Name'
+        organization_type = 'For-profit business'
+        description = 'New test description'
+        dimension = 'Medium'
+        headquarters = 'Milan'
+        website = 'http://www.example.com'
+
+        #Update the profile of the organization
+        r = self.app.post('%sadmin/organizationprofile/change_data' % self.org.url(),
+            params = dict(
+                fullname = fullname,
+                organization_type = organization_type,
+                description = description,
+                dimension = dimension,
+                headquarters = headquarters,
+                website = website),
+            extra_environ=dict(username='test-user-1'))
+
+        ThreadLocalORMSession.flush_all()
+
+        r = self.app.get('%sorganizationprofile' % self.org.url())
+        assert organization_type in r
+        assert description in r
+        assert headquarters in r
+        assert fullname in r
+        assert website in r
+
+        self.org = OM.Organization.query.get(_id=self.org._id)
+
+        assert self.org.organization_type == organization_type
+        assert self.org.description == description
+        assert self.org.dimension == dimension
+        assert self.org.headquarters == headquarters
+        assert self.org.website == website
+        assert self.org.fullname == fullname
+
+        #Try to provide invalid parameters
+        r = self.app.post('%sadmin/organizationprofile/change_data' % self.org.url(),
+            params = dict(
+                fullname = 'a',
+                organization_type = 'Invalid type',
+                description = 'b',
+                dimension = 'Invalid dimension',
+                headquarters = 'c',
+                website = 'd'),
+            extra_environ=dict(username='test-user-1'))
+
+        ThreadLocalORMSession.flush_all()
+
+        r = self.app.get('%sorganizationprofile' % self.org.url(),
+            extra_environ=dict(username='test-user-1'))
+
+        self.org = OM.Organization.query.get(_id=self.org._id)
+        assert self.org.organization_type == organization_type
+        assert self.org.description == description
+        assert self.org.dimension == dimension
+        assert self.org.headquarters == headquarters
+        assert self.org.website == website
+        assert self.org.fullname == fullname
+
+
+    @td.with_user_project('test-user-1')
+    def test_workfield(self):
+
+        wf = OM.WorkFields.query.get(name='Mobile apps')
+        c.user = User.by_username('test-user-1')
+
+        #Add a workfield
+        r = self.app.post('%sadmin/organizationprofile/add_work_field' % self.org.url(),
+            params = dict(
+                workfield = str(wf._id)),
+            extra_environ=dict(username='test-user-1'))
+
+        r = self.app.get('%sorganizationprofile' % self.org.url())
+        
+        self.org = OM.Organization.query.get(_id=self.org._id)
+        assert len(self.org.getWorkfields()) == 1
+        assert self.org.getWorkfields()[0]._id == wf._id
+        assert wf.name in r
+        assert wf.description in r
+        
+        #Add a second workfield
+        wf2 = OM.WorkFields.query.get(name='Web applications')
+
+        r = self.app.post('%sadmin/organizationprofile/add_work_field' % self.org.url(),
+            params = dict(
+                workfield = str(wf2._id)),
+            extra_environ=dict(username='test-user-1'))
+
+        r = self.app.get('%sorganizationprofile' % self.org.url())
+        
+        self.org = OM.Organization.query.get(_id=self.org._id)
+        assert len(self.org.getWorkfields()) == 2
+        assert wf2.name in r
+        assert wf2.description in r
+
+        #Remove a workfield
+        r = self.app.post('%sadmin/organizationprofile/remove_work_field' % self.org.url(),
+            params = {'workfieldid' : str(wf._id)},
+            extra_environ=dict(username='test-user-1'))
+
+        r = self.app.get('%sorganizationprofile' % self.org.url())
+        assert len(self.org.getWorkfields()) == 1
+        assert self.org.getWorkfields()[0]._id == wf2._id
+        assert wf.name not in r
+        assert wf.description not in r
+
+class TestOrganizationMembership(TestOrganization):
+
+    @td.with_user_project('test-user-1')
+    def test_invite_user(self):
+        #Try to invite a new user
+        user3 = User.by_username('test-admin')
+        testrole = 'Software Engineer'
+
+        r = self.app.post('%sadmin/organizationprofile/invite_user' % self.org.url(),
+            params=dict(
+                username = user3.username,
+                role = testrole),
+            extra_environ=dict(username='test-user-1'))
+        
+        r = self.app.get('%sadmin/organizationprofile' % self.org.url(),
+            extra_environ=dict(username='test-user-1'))
+        assert user3.display_name in r
+
+        m = OM.Membership.query.get(
+            member_id=user3._id, 
+            organization_id=self.org._id)
+        assert m.status == 'invitation'
+        assert m.role == testrole
+
+        #Accept invitation
+        r = self.app.post('/organization/change_membership',
+            params=dict(
+                status = 'active',
+                membershipid = str(m._id),
+                role = testrole),
+            extra_environ=dict(username='test-admin'))
+
+        m = OM.Membership.query.get(
+            member_id=user3._id, 
+            organization_id=self.org._id)
+        assert m.status == 'active'
+        assert m.role == testrole
+        
+    @td.with_user_project('test-user-1')
+    def test_change_permissions(self):
+        m = OM.Membership.query.get(
+            member_id=self.user2._id, 
+            organization_id=self.org._id)
+
+        #Close the involvement of test-user-1
+        testuser1 = User.by_username('test-user-1')
+        m = OM.Membership.query.get(
+            member_id=testuser1._id, 
+            organization_id=self.org._id)
+
+        r = self.app.post('%sadmin/organizationprofile/change_membership' % self.org.url(),
+            params=dict(
+                status = 'closed',
+                membershipid = str(m._id),
+                requestfrom = 'user',
+                role = self.role),
+            extra_environ=dict(username='test-user-1'))
+
+        m = OM.Membership.query.get(
+            member_id=c.user._id, 
+            organization_id=self.org._id)
+        assert m.status == 'closed'
+        assert m.role == self.role
+
+
+    @td.with_user_project('test-admin')
+    def test_send_request(self):
+        #Send an admission request from a new user
+        user3 = User.by_username('test-admin')
+        testrole = 'Software Engineer'
+
+        r = self.app.post('%sorganizationprofile/admission_request' % self.org.url(),
+            params=dict(
+                role = testrole),
+            extra_environ=dict(username='test-admin'))
+
+        c.user = M.User.by_username('test-user-1')
+        r = self.app.get('%sadmin/organizationprofile' % self.org.url(),
+            extra_environ=dict(username='test-user-1'))
+        assert user3.display_name in r
+
+        m = OM.Membership.query.get(
+            member_id=user3._id, 
+            organization_id=self.org._id)
+        assert m.status == 'request'
+        assert m.role == testrole
+
+        #Accept request
+        r = self.app.post('%sadmin/organizationprofile/change_membership' % self.org.url(),
+            params=dict(
+                status = 'active',
+                membershipid = str(m._id),
+                requestfrom = 'user',
+                role = testrole),
+            extra_environ=dict(username='test-user-1'))
+
+        m = OM.Membership.query.get(
+            member_id=user3._id, 
+            organization_id=self.org._id)
+        assert m.status == 'active'
+        assert m.role == testrole
+
+#check projects
+class TestOrganizationProjects(TestOrganization):
+
+    @td.with_user_project('test-admin')
+    def test_request_collaboration(self):
+        def send_request(self):
+            #Try to send a request to participate to a project
+            #The user sending the request is the admin of the org's profile
+            r = self.app.post('/organizationstool/send_request',
+                params=dict(
+                    organization = str(self.org._id),
+                    coll_type = 'cooperation'),
+                extra_environ=dict(username='test-user-1'))
+            return OM.ProjectInvolvement.query.get(organization_id=self.org._id)
+       
+        p = send_request(self)
+        assert p.status == 'request'
+        assert p.collaborationtype == 'cooperation'
+
+        #As the admin of the project, reject pending request
+        r = self.app.post('/organizationstool/update_collaboration_status',
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'remove'),
+            extra_environ=dict(username='test-admin'))
+
+        p = OM.ProjectInvolvement.query.get(organization_id=self.org._id)        
+        assert p is None
+
+        p = send_request(self)
+
+        #As the admin of the project, accept pending request
+        r = self.app.post('/organizationstool/update_collaboration_status',
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'active'),
+            extra_environ=dict(username='test-admin'))
+        
+        assert p.status == 'active'
+        assert p.collaborationtype == 'cooperation'
+
+        #As the admin of the project, close the collaboration
+        r = self.app.post('/organizationstool/update_collaboration_status',
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'closed'),
+            extra_environ=dict(username='test-admin'))
+        
+        p = OM.ProjectInvolvement.query.get(organization_id=self.org._id)        
+        assert p.status == 'closed'
+        assert p.collaborationtype == 'cooperation'
+
+    @td.with_user_project('test-admin')
+    def test_invite_organization(self):
+        def send_invitation(self):
+            #Try to send an invitation to participate to a project
+            #The user sending the request is the admin of the project
+            r = self.app.post('/organizationstool/invite',
+                params=dict(
+                    organizationid = str(self.org._id),
+                    collaborationtype = 'cooperation'),
+                extra_environ=dict(username='test-admin'))
+            return OM.ProjectInvolvement.query.get(organization_id=self.org._id)
+       
+        p = send_invitation(self)
+        assert p.status == 'invitation'
+        assert p.collaborationtype == 'cooperation'
+
+        #As the admin of the organization, reject pending invitation
+        r = self.app.post('%sadmin/organizationprofile/update_collaboration_status' % self.org.url(),
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'remove'),
+            extra_environ=dict(username='test-user-1'))
+
+        p = OM.ProjectInvolvement.query.get(organization_id=self.org._id)        
+        assert p is None
+
+        p = send_invitation(self)
+
+        #As the admin of the organization, accept pending invitation
+        r = self.app.post('%sadmin/organizationprofile/update_collaboration_status' % self.org.url(),
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'active'),
+            extra_environ=dict(username='test-user-1'))
+        
+        assert p.status == 'active'
+        assert p.collaborationtype == 'cooperation'
+
+        #As the admin of the organization, close the collaboration
+        r = self.app.post('%sadmin/organizationprofile/update_collaboration_status' % self.org.url(),
+            params=dict(
+                collaborationid = str(p._id),
+                collaborationtype = 'cooperation',
+                status = 'closed'),
+            extra_environ=dict(username='test-user-1'))
+        
+        p = OM.ProjectInvolvement.query.get(organization_id=self.org._id)        
+        assert p.status == 'closed'
+        assert p.collaborationtype == 'cooperation'

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/controller/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/controller/__init__.py b/ForgeOrganization/forgeorganization/tool/controller/__init__.py
new file mode 100644
index 0000000..1771655
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/controller/__init__.py
@@ -0,0 +1 @@
+from organizationtool import OrganizationToolController

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/controller/organizationtool.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/controller/organizationtool.py b/ForgeOrganization/forgeorganization/tool/controller/organizationtool.py
new file mode 100644
index 0000000..af64a81
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/controller/organizationtool.py
@@ -0,0 +1,144 @@
+from tg import expose, validate, redirect, flash
+from tg.decorators import with_trailing_slash
+from pylons import c
+from allura.lib import validators as V
+from allura.lib.decorators import require_post
+from allura import model as M
+from allura.lib.security import require_authenticated
+from allura.controllers import BaseController
+import forgeorganization.tool.widgets.forms as forms 
+from forgeorganization.organization.model import Organization
+from forgeorganization.organization.model import ProjectInvolvement
+import re
+from datetime import datetime
+
+class Forms(object):
+    search_form=forms.SearchOrganizationForm(action='search')
+    invite_form=forms.InviteOrganizationForm(action='invite')
+    collaboration_request_form=forms.SendCollaborationRequestForm(
+        action='send_request')
+    def new_change_status_form(self):
+        return forms.ChangeCollaborationStatusForm(
+            action='update_collaboration_status')
+
+F = Forms()
+
+class OrganizationToolController(BaseController):
+
+    @expose('jinja:forgeorganization:tool/templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        is_admin=c.user.username in c.project.admins()
+        cooperations=[el for el in c.project.organizations
+                      if el.collaborationtype=='cooperation']
+        participations=[el for el in c.project.organizations
+                        if el.collaborationtype=='participation']
+        user_organizations=[o for o in Organization.query.find()
+                            if c.user.username in o.project().admins()]
+        return dict(
+            user_organizations=user_organizations,
+            cooperations=cooperations, 
+            participations=participations,
+            is_admin=is_admin,
+            forms = F)
+
+    @expose('jinja:forgeorganization:tool/templates/search_results.html')
+    @require_post()
+    @with_trailing_slash
+    @validate(F.search_form, error_handler=index)
+    def search(self, organization, **kw):
+        regx = re.compile(organization, re.IGNORECASE)
+        orgs = Organization.query.find(dict(fullname=regx))
+        return dict(
+            orglist = orgs, 
+            forms = F,
+            search_string = organization)
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def invite(self, organizationid, collaborationtype, **kw):
+        require_authenticated()
+        is_admin=c.user.username in c.project.admins()
+        if not is_admin:
+            flash("You are not allowed to perform this action", "error")
+            redirect(".")
+            return
+        org = Organization.getById(organizationid)
+        if not org:
+            flash("Invalid organization.", "error")
+            redirect(".")
+            return
+        result = ProjectInvolvement.insert(
+            status='invitation', 
+            collaborationtype=collaborationtype, 
+            organization_id=organizationid, 
+            project_id=c.project._id)
+        if not result:
+            flash("This organization is already involved in this project.",
+                  "error")
+        else:
+            flash("Invitation correctly sent.")
+        redirect(".")
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def update_collaboration_status(self, collaborationid, collaborationtype, status, **kw):
+        require_authenticated()
+        is_admin=c.user.username in c.project.admins()
+        if not is_admin:
+            flash("You are not allowed to perform this action", "error")
+            redirect(".")
+            return
+
+        allowed = True
+        coll = ProjectInvolvement.getById(collaborationid)
+        if not coll:
+            allowed = False
+        if coll.status != status:
+            if coll.status=='invitation' and status!='remove':
+                allowed=False
+            elif coll.status=='closed':
+                allowed=False
+            elif coll.status=='active' and status!='closed':
+                allowed=False
+            elif coll.status=='request' and status not in ['active','remove']:
+                allowed=False
+
+        if allowed:
+            if status=='closed':
+                collaborationtype=coll.collaborationtype
+            if status=='remove':
+                ProjectInvolvement.delete(coll._id)
+            else:
+                coll.collaborationtype=collaborationtype
+                coll.setStatus(status)
+            flash('The information about this collaboration has been updated')
+        else:
+            flash("You are not allowed to perform this action", "error")
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def send_request(self, organization, coll_type, **kw):
+        organization = Organization.getById(organization)     
+
+        if (not organization) or not (c.user.username in organization.project().admins()):
+            flash("You are not allowed to perform this action.", "error")
+            redirect(".")
+            return
+        
+        for org in c.project.organizations:
+            if org.organization == organization and org.status!='closed':
+                flash(
+                    "This organization is already included in this project.",
+                    "error")
+                redirect(".")
+                return
+        ProjectInvolvement.insert('request', coll_type, organization._id, 
+            c.project._id)
+        flash("Your collaboration request was successfully sent.")
+        redirect(".")
+        return

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/main.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/main.py b/ForgeOrganization/forgeorganization/tool/main.py
new file mode 100644
index 0000000..cf866ee
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/main.py
@@ -0,0 +1,91 @@
+#-*- python -*-
+import logging
+from pylons import c
+import formencode
+from formencode import validators
+from webob import exc
+
+from allura.app import Application, SitemapEntry
+from allura.lib import helpers as h
+from allura.lib.security import has_access
+from allura import model as M
+
+from forgeorganization import version
+from forgeorganization.tool.controller import OrganizationToolController
+
+from ming.orm import session
+
+log = logging.getLogger(__name__)
+
+class ForgeOrganizationToolApp(Application):
+    __version__ = version.__version__
+    tool_label='Organizations'
+    default_mount_label='Organizations'
+    default_mount_point='organizations'
+    permissions = ['configure', 'read', 'write',
+                    'unmoderated_post', 'post', 'moderate', 'admin']
+    ordinal=15
+    installable=False
+    config_options = Application.config_options
+    default_external_feeds = []
+    icons={
+        24:'images/org_24.png',
+        32:'images/org_32.png',
+        48:'images/org_48.png'
+    }
+    root = OrganizationToolController()
+
+    def __init__(self, project, config):
+        Application.__init__(self, project, config)
+        role_admin = M.ProjectRole.by_name('Admin')._id
+        role_anon = M.ProjectRole.by_name('*anonymous')._id
+        self.config.acl = [
+            M.ACE.allow(role_anon, 'read'),
+            M.ACE.allow(role_admin, 'admin')]
+
+    def main_menu(self):
+        return [SitemapEntry(self.config.options.mount_label.title(), '.')]
+
+    @property
+    @h.exceptionless([], log)
+    def sitemap(self):
+        menu_id = self.config.options.mount_label.title()
+        with h.push_config(c, app=self):
+            return [
+                SitemapEntry(menu_id, '.')[self.sidebar_menu()] ]
+
+    @property
+    def show_discussion(self):
+        if 'show_discussion' in self.config.options:
+            return self.config.options['show_discussion']
+        else:
+            return True
+
+    @h.exceptionless([], log)
+    def sidebar_menu(self):
+        base = c.app.url
+        links = [SitemapEntry('Home', base)]
+        return links
+
+    def admin_menu(self):
+        admin_url=c.project.url()+'admin/'+self.config.options.mount_point+'/'
+        links = [SitemapEntry(
+                     'Involved organizations',
+                     admin_url + 'edit_label', 
+                     className='admin_modal')]
+        return links
+
+    def install(self, project):
+        #It doesn't make any sense to install the tool twice on the same 
+        #project therefore, if it already exists, it doesn't install it
+        #a second time.
+        for tool in project.app_configs:
+            if tool.tool_name == 'organizationstool':
+                if self.config.options.mount_point!=tool.options.mount_point:
+                    project.uninstall_app(self.config.options.mount_point)
+                    return
+
+    def uninstall(self, project):
+        self.config.delete()
+        session(self.config).flush()
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/templates/index.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/templates/index.html b/ForgeOrganization/forgeorganization/tool/templates/index.html
new file mode 100644
index 0000000..8092a39
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/templates/index.html
@@ -0,0 +1,114 @@
+{% extends g.theme.master %}
+
+{% block title %}Organizations involved in {{c.project.name}}{% endblock %}
+
+{% block header %}Organizations involved in {{c.project.name}}{% endblock %}
+
+{% block content %}
+
+  <div class="grid-20">
+    <h2>Involved organizations</h2>
+  </div>
+
+  <div class="grid-20">
+    <h3>Organizations cooperating at the project as main partners</h3>
+    {% if cooperations %}
+      {% if is_admin %}
+        <table>
+          <thead>
+            <tr>
+              <th>Organization</th>
+              <th>Type</th>
+              <th>Status</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for oc in cooperations %}
+              <tr>{{forms.new_change_status_form().display(collaboration=oc)}}</tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% else %}
+        <table>
+          <thead>
+            <tr>
+              <th>Organization</th>
+              <th>Status</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for oc in cooperations %}
+              <tr>
+                <td><a href="{{oc.organization.url()}}">{{oc.organization.fullname}}</a></td>
+                <td>{{oc.status.capitalize()}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% endif %}
+    {% else %}
+      <p>There are no organizations involved as main cooperators in this project.</p>
+    {% endif %}
+  </div>
+
+  <div class="grid-20">
+    <h3>Other organizations participating at the project</h3>
+    {% if participations %}
+      {% if is_admin %}
+        <table>
+          <thead>
+            <tr>
+              <th>Organization</th>
+              <th>Type</th>
+              <th>Status</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for oc in participations %}
+              <tr>{{forms.new_change_status_form().display(collaboration=oc)}}</tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% else %}
+        <table>
+          <thead>
+            <tr>
+              <th>Organization</th>
+              <th>Status</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for oc in participations %}
+              <tr>
+                <td><a href="{{oc.organization.url()}}">{{oc.organization.fullname}}</a></td>
+                <td>{{oc.status.capitalize()}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% endif %}
+    {% else %}
+      <p>There are no organizations involved as participants in this project.</p>
+    {% endif %}
+  </div>
+
+  {% if is_admin %}
+    <div class="grid-20">
+      <h2>Invite an organization</h2>
+    </div>
+    <div class="grid-20">
+      {{forms.search_form.display()}}
+    </div>
+  {% else %}
+    {% if user_organizations %}
+      <div class="grid-20">
+        <h2>Send collaboration request</h2>
+      </div>
+      <div class="grid-20">
+        {{forms.collaboration_request_form.display(organizations=user_organizations)}}
+      </div>
+    {% endif %}
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/templates/search_results.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/templates/search_results.html b/ForgeOrganization/forgeorganization/tool/templates/search_results.html
new file mode 100644
index 0000000..4b4eee2
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/templates/search_results.html
@@ -0,0 +1,37 @@
+{% extends g.theme.master %}
+
+{% block title %}Invite an organization to collaborate to {{c.project.name}}{% endblock %}
+
+{% block header %}Invite an organization to collaborate to {{c.project.name}}{% endblock %}
+
+{% block content %}
+
+  <div class="grid-20">
+    <h2>Search results for {{search_string}}</h2>
+  </div>
+
+  <div class="grid-20">
+    {%if orglist%}
+      <table>
+        <thead>
+          <th>Organization</th>
+          <th>Collaboration type</th>
+          <th>Action</th>
+        </thead>
+        <tbody>
+          {%for org in orglist%}
+            {{forms.invite_form.display(organization=org)}}
+          {%endfor%}
+        </tbody>
+      </table>
+    {%else%}
+      <p>No result was found matching the string "{{search_string}}".</p>
+    {%endif%}
+  </div>
+
+  <div class="grid-20">
+    <h2>Search again</h2>
+    {{forms.search_form.display()}}
+  </div>
+  
+{% endblock %}

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/tool/widgets/forms.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/tool/widgets/forms.py b/ForgeOrganization/forgeorganization/tool/widgets/forms.py
new file mode 100644
index 0000000..6d50fea
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/tool/widgets/forms.py
@@ -0,0 +1,152 @@
+from allura.lib import validators as V
+from allura.lib.widgets.forms import ForgeForm
+
+from formencode import validators as fev
+
+import ew as ew_core
+import ew.jinja2_ew as ew
+
+class SearchOrganizationForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text='Search')
+
+    class fields(ew_core.NameList):
+        organization=ew.TextField(
+            label='Organization',
+            validator=fev.UnicodeString(not_empty=True))
+
+class InviteOrganizationForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        org = kw.get('organization')
+        orgnamefield = '<a href="%s">%s</a>' % (org.url(), org.fullname)
+
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="organizationid",
+                        attrs={'value':str(org._id)},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=orgnamefield,
+                        show_errors=False),
+                    ew.SingleSelectField(
+                        name='collaborationtype',
+                        options = [
+                            ew.Option(
+                                py_value='cooperation', 
+                                label='Cooperation'),
+                            ew.Option(
+                                py_value='participation',
+                                label='Participation')],
+                        validator=fev.UnicodeString(not_empty=True)),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Invite'},
+                        show_errors=False)])]
+        return super(InviteOrganizationForm, self).display(**kw)
+
+class SendCollaborationRequestForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text='Send')
+
+    class fields(ew_core.NameList):
+        organization = ew.SingleSelectField(
+            label='Organization',
+            options = [],
+            validator=fev.UnicodeString(not_empty=True))
+
+        coll_type = ew.SingleSelectField(
+            label='Collaboration Type',
+            options = [
+                ew.Option(
+                    py_value='cooperation', 
+                    label='Cooperation'),
+                ew.Option(
+                    py_value='participation',
+                    label='Participation')],
+             validator=fev.UnicodeString(not_empty=True))
+
+    def display(self, **kw):
+        orgs = kw.get('organizations')
+        opts=[ew.Option(py_value=org._id,label=org.fullname) for org in orgs]
+        self.fields['organization'].options = opts
+        return super(SendCollaborationRequestForm, self).display(**kw)
+
+class ChangeCollaborationStatusForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        coll = kw.get('collaboration')
+        org = coll.organization
+        orgnamefield = '<a href="%s">%s</a>' % (org.url(), org.fullname)
+        
+        select_cooperation = (coll.collaborationtype=='cooperation')
+        if coll.status=='closed':
+            options=[ew.Option(py_value='closed',label='Closed',selected=True)]
+        elif coll.status=='active':
+            options=[
+                ew.Option(py_value='closed',label='Closed',selected=False),
+                ew.Option(py_value='active',label='Active',selected=True)]
+        elif coll.status=='invitation':
+            options=[
+                ew.Option(
+                    py_value='invitation',
+                    label='Pending invitation',
+                    selected=True),
+                ew.Option(
+                    py_value='remove',
+                    label='Remove invitation',
+                    selected=False)]
+        elif coll.status=='request':
+            options=[
+                ew.Option(
+                    py_value='request',
+                    label='Pending request',
+                    selected=True),
+                ew.Option(
+                    py_value='remove',
+                    label='Decline request',
+                    selected=False),
+                ew.Option(
+                    py_value='active',
+                    label='Accept request',
+                    selected=False)]
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="collaborationid",
+                        attrs={'value':str(coll._id)},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=orgnamefield,
+                        show_errors=False),
+                    ew.SingleSelectField(
+                        name='collaborationtype',
+                        options = [
+                            ew.Option(
+                                py_value='cooperation', 
+                                selected=select_cooperation,
+                                label='Cooperation'),
+                            ew.Option(
+                                py_value='participation',
+                                selected=not select_cooperation,
+                                label='Participation')],
+                        validator=fev.UnicodeString(not_empty=True)),
+                    ew.SingleSelectField(
+                        name='status',
+                        options = options,
+                        validator=fev.UnicodeString(not_empty=True)),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Save'},
+                        show_errors=False)])]
+        return super(ChangeCollaborationStatusForm, self).display(**kw)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/version.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/version.py b/ForgeOrganization/forgeorganization/version.py
new file mode 100644
index 0000000..6514373
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/version.py
@@ -0,0 +1,2 @@
+__version_info__ = (0, 0)
+__version__ = '.'.join(map(str, __version_info__))

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/setup.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/setup.py b/ForgeOrganization/setup.py
new file mode 100644
index 0000000..e7389d6
--- /dev/null
+++ b/ForgeOrganization/setup.py
@@ -0,0 +1,33 @@
+from setuptools import setup, find_packages
+import sys, os
+
+from forgeorganization.version import __version__
+
+setup(name='ForgeOrganization',
+      version=__version__,
+      description="",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='',
+      author_email='',
+      url='',
+      license='',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+          'allura',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [allura.organization]
+      organization=forgeorganization.organization.main:ForgeOrganizationApp
+
+      [allura]
+      organizationprofile = forgeorganization.organization_profile.organization_main:OrganizationProfileApp
+      organizationstool=forgeorganization.tool.main:ForgeOrganizationToolApp
+      """,
+      )

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/test.ini
----------------------------------------------------------------------
diff --git a/ForgeOrganization/test.ini b/ForgeOrganization/test.ini
new file mode 100644
index 0000000..dd41628
--- /dev/null
+++ b/ForgeOrganization/test.ini
@@ -0,0 +1,57 @@
+#
+# allura - TurboGears 2 testing environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = true
+organizations.enable = true
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5000
+
+[app:main]
+use = config:../Allura/test.ini
+
+[app:main_without_authn]
+use = config:../Allura/test.ini#main_without_authn
+
+[app:main_with_amqp]
+use = config:../Allura/test.ini#main_with_amqp
+
+[loggers]
+keys = root, allura, tool
+
+[handlers]
+keys = test
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = test
+
+[logger_allura]
+level = DEBUG
+handlers =
+qualname = allura
+
+[logger_tool]
+level = DEBUG
+handlers =
+qualname = forgeorganization
+
+[handler_test]
+class = FileHandler
+args = ('test.log',)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
+
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/.svn/all-wcprops b/ForgeOrganizationStats/.svn/all-wcprops
new file mode 100644
index 0000000..3f221cf
--- /dev/null
+++ b/ForgeOrganizationStats/.svn/all-wcprops
@@ -0,0 +1,11 @@
+K 25
+svn:wc:ra_dav:version-url
+V 37
+/svn/allura/!svn/ver/3/ForgeUserStats
+END
+setup.py
+K 25
+svn:wc:ra_dav:version-url
+V 46
+/svn/allura/!svn/ver/1/ForgeUserStats/setup.py
+END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/.svn/entries b/ForgeOrganizationStats/.svn/entries
new file mode 100644
index 0000000..a6a6ad7
--- /dev/null
+++ b/ForgeOrganizationStats/.svn/entries
@@ -0,0 +1,65 @@
+10
+
+dir
+4
+https://xp-dev.com/svn/allura/ForgeUserStats
+https://xp-dev.com/svn/allura
+
+
+
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+46ed536d-f66c-413e-a53e-834384f708db
+
+forgeuserstats
+dir
+
+setup.py
+file
+
+
+
+
+2012-11-05T14:43:25.737756Z
+304c2b65bdf3d07e2f434208b164af4f
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+775
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/.svn/text-base/setup.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/.svn/text-base/setup.py.svn-base b/ForgeOrganizationStats/.svn/text-base/setup.py.svn-base
new file mode 100644
index 0000000..dc2f07b
--- /dev/null
+++ b/ForgeOrganizationStats/.svn/text-base/setup.py.svn-base
@@ -0,0 +1,29 @@
+from setuptools import setup, find_packages
+import sys, os
+
+from forgeuserstats.version import __version__
+
+setup(name='ForgeUserStats',
+      version=__version__,
+      description="",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='',
+      author_email='',
+      url='',
+      license='',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+          'allura',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [allura.stats]
+      userstats=forgeuserstats.main:ForgeUserStatsApp
+      """,
+      )

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/.svn/all-wcprops b/ForgeOrganizationStats/forgeorganizationstats/.svn/all-wcprops
new file mode 100644
index 0000000..822e983
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/.svn/all-wcprops
@@ -0,0 +1,23 @@
+K 25
+svn:wc:ra_dav:version-url
+V 52
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 64
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/__init__.py
+END
+main.py
+K 25
+svn:wc:ra_dav:version-url
+V 60
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/main.py
+END
+version.py
+K 25
+svn:wc:ra_dav:version-url
+V 63
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/version.py
+END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/.svn/entries b/ForgeOrganizationStats/forgeorganizationstats/.svn/entries
new file mode 100644
index 0000000..d934292
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/.svn/entries
@@ -0,0 +1,136 @@
+10
+
+dir
+4
+https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats
+https://xp-dev.com/svn/allura
+
+
+
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+46ed536d-f66c-413e-a53e-834384f708db
+
+main.py
+file
+
+
+
+
+2012-11-05T14:43:25.733756Z
+9c5d8215ba783648e8e8682a00d519cc
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+8711
+
+version.py
+file
+
+
+
+
+2012-11-05T14:43:25.729756Z
+b5b12fd0365a9c5043213c216f3e889b
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+77
+
+templates
+dir
+
+model
+dir
+
+__init__.py
+file
+
+
+
+
+2012-11-05T14:43:25.733756Z
+f8653a729acb235bf0939944a047199a
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+42
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/__init__.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/__init__.py.svn-base b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..34738da
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/__init__.py.svn-base
@@ -0,0 +1 @@
+from main import ForgeUserStatsController

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/main.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/main.py.svn-base b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/main.py.svn-base
new file mode 100644
index 0000000..949ae74
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/main.py.svn-base
@@ -0,0 +1,227 @@
+import logging
+
+import pylons
+pylons.c = pylons.tmpl_context
+pylons.g = pylons.app_globals
+from pylons import c, g, request
+from tg import expose, validate, config
+from tg.decorators import with_trailing_slash
+from paste.deploy.converters import asbool
+from webob import exc
+from datetime import datetime, date, timedelta
+
+from allura.app import Application, SitemapEntry
+from allura.eventslistener import EventsListener
+from allura import version
+from allura.controllers import BaseController
+from allura.lib.security import require_authenticated
+from model.stats import UserStats
+import allura.model as M
+
+from ming.orm.ormsession import ThreadLocalORMSession
+
+log = logging.getLogger(__name__)
+
+class ForgeUserStatsController(BaseController):
+
+    @expose()
+    def _lookup(self, part, *remainder):
+        user = M.User.query.get(username=part)
+
+        if not hasattr(c, 'userstats') :
+            c.userstats = user
+            c.category = None
+            return ForgeUserStatsController(), remainder
+        if part == "category" :
+            return ForgeUserStatsCatController(), remainder
+        if part == "metric" :
+            return ForgeUserStatsMetricController(), remainder
+
+    @expose('jinja:forgeuserstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        if not c.userstats : return dict(user=None)
+        stats = c.userstats.stats[0]
+
+        ret_dict = _getDataForCategory(None)
+        ret_dict['user'] = c.userstats
+        ret_dict['registration_date'] = stats.registration_date
+
+        ret_dict['totlogins'] = stats.tot_logins_count
+        ret_dict['last_login'] = stats.last_login
+        if stats.last_login :
+          ret_dict['last_login_days'] = (datetime.utcnow()-stats.last_login).days
+
+        categories = {}
+        for p in c.userstats.my_projects() :
+            for cat in p.trove_topic :
+                cat = M.TroveCategory.query.get(_id = cat)
+                if categories.get(cat) : categories[cat] += 1
+                else : categories[cat] = 1
+        categories = sorted(categories.items(), key=lambda (x,y) : y,reverse=True)
+
+        ret_dict['lastmonth_logins'] = stats.getLastMonthLogins()
+        ret_dict['categories'] = categories
+        days = ret_dict['days']
+        if days >= 30 : 
+            ret_dict['permonthlogins'] = \
+              round(stats.tot_logins_count*30.0/days,2)
+        else : ret_dict['permonthlogins'] = 'n/a'
+
+
+        ret_dict['codestars'] = stats.codeRanking()
+        ret_dict['discussionstars'] = stats.discussionRanking()
+        ret_dict['ticketsstars'] = stats.ticketsRanking()
+        return ret_dict
+
+class ForgeUserStatsCatController(BaseController):
+    @expose()
+    def _lookup(self, category, *remainder):
+        c.category = M.TroveCategory.query.get(fullname=category)
+        return ForgeUserStatsCatController(), remainder
+
+    @expose('jinja:forgeuserstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        if not c.userstats : return dict(user=None)
+        stats = c.userstats.stats[0]
+        
+        cat_id = None
+        if c.category : cat_id = c.category._id
+        ret_dict = _getDataForCategory(cat_id)
+        ret_dict['user'] = c.userstats
+        ret_dict['registration_date'] = stats.registration_date
+        ret_dict['category'] = c.category
+        
+        return ret_dict
+
+class ForgeUserStatsMetricController(BaseController):
+
+    @expose('jinja:forgeuserstats:templates/commits.html')
+    @with_trailing_slash
+    def commits(self, **kw):
+        if not c.userstats : return dict(user=None)
+        stats = c.userstats.stats[0]
+        
+        commits = stats.getCommitsByCategory()
+        return dict(user = c.userstats,
+                    data = commits) 
+
+    @expose('jinja:forgeuserstats:templates/artifacts.html')
+    @with_trailing_slash
+    def artifacts(self, **kw):
+        if not c.userstats : return dict(user=None)
+
+        stats = c.userstats.stats[0]        
+        artifacts = stats.getArtifactsByCategory(detailed=True)
+        return dict(user = c.userstats,
+                    data = artifacts) 
+
+    @expose('jinja:forgeuserstats:templates/tickets.html')
+    @with_trailing_slash
+    def tickets(self, **kw):
+        if not c.userstats : return dict(user=None)
+
+        stats = c.userstats.stats[0]        
+        artifacts = stats.getTicketsByCategory()
+        return dict(user = c.userstats,
+                    data = artifacts) 
+
+def _getDataForCategory(category) :
+    stats = c.userstats.stats[0]
+    totcommits = stats.getCommits(category)
+    tottickets = stats.getTickets(category)
+    averagetime = tottickets.get('averagesolvingtime')
+    artifacts_by_type = stats.getArtifactsByType(category)
+    totartifacts = artifacts_by_type.get(None) 
+    if totartifacts : del artifacts_by_type[None]
+    else : totartifacts = {'created' : 0, 'modified' : 0}
+    lmcommits = stats.getLastMonthCommits(category)
+    lm_artifacts_by_type = stats.getLastMonthArtifactsByType(category)
+    lm_totartifacts = stats.getLastMonthArtifacts(category)
+    lm_tickets = stats.getLastMonthTickets(category)
+
+    averagetime = lm_tickets.get('averagesolvingtime')
+
+    days = (datetime.utcnow() - stats.registration_date).days
+    if days >= 30 : 
+        pmartifacts = {'created' : round(totartifacts['created']*30.0/days,2),
+                       'modified': round(totartifacts['modified']*30.0/days,2)}
+        pmcommits = {'number': round(totcommits['number']*30.0/days,2),
+                     'lines' : round(totcommits['lines']*30.0/days,2)}
+        pmtickets = {'assigned' : round(tottickets['assigned']*30.0/days,2),
+                     'revoked' : round(tottickets['revoked']*30.0/days,2),
+                     'solved' : round(tottickets['solved']*30.0/days,2),
+                     'averagesolvingtime' : 'n/a'}
+        for key in artifacts_by_type :
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = \
+                round(value['created']*30.0/days,2)
+            artifacts_by_type[key]['pmmodified']= \
+                round(value['modified']*30.0/days,2)
+    else : 
+        pmartifacts = {'created'  : 'n/a',
+                       'modified' : 'n/a'}
+        pmcommits = {'number': 'n/a',
+                         'lines' : 'n/a'}
+        pmtickets = {'assigned'           : 'n/a',
+                     'revoked'            : 'n/a',
+                     'solved'             : 'n/a',
+                     'averagesolvingtime' : 'n/a'}
+        for key in artifacts_by_type :
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = 'n/a'
+            artifacts_by_type[key]['pmmodified']= 'n/a'
+
+    return dict(days = days,
+                totcommits = totcommits,
+                lastmonthcommits = lmcommits,
+                lastmonthtickets = lm_tickets,
+                tottickets = tottickets,
+                permonthcommits = pmcommits,
+                totartifacts = totartifacts,
+                lastmonthartifacts = lm_totartifacts,
+                permonthartifacts = pmartifacts,
+                artifacts_by_type = artifacts_by_type,
+                lastmonth_artifacts_by_type = lm_artifacts_by_type,
+                permonthtickets = pmtickets)
+
+class UserStatsListener(EventsListener) :
+    def newArtifact(self, art_type, art_datetime, project, user):
+        stats = UserStats.query.get(userid=user._id)
+        stats.addNewArtifact(art_type, art_datetime, project)
+
+    def modifiedArtifact(self, art_type, art_datetime, project, user):
+        stats = UserStats.query.get(userid=user._id)
+        stats.addModifiedArtifact(art_type, art_datetime, project)
+
+    def newUser(self, user):
+        UserStats(userid=user._id,
+                  registration_date = datetime.utcnow())
+
+    def ticketEvent(self, event_type, ticket, project, user):
+        if user is None : return
+        stats = UserStats.query.get(userid=user._id)
+        if event_type == "assigned" : 
+            stats.addAssignedTicket(ticket, project)
+        elif event_type == "revoked" :
+            stats.addRevokedTicket(ticket, project)
+        elif event_type == "closed" :
+            stats.addClosedTicket(ticket, project)
+
+    def newCommit(self, newcommit, project, user):
+        stats = user.stats[0]
+        stats.addCommit(newcommit, project)
+
+    def addUserLogin(self, user):
+        stats = user.stats[0]
+        stats.addLogin()
+
+class ForgeUserStatsApp :
+    root = ForgeUserStatsController()
+    listener = UserStatsListener()
+
+    @classmethod
+    def createlink(cls, user) :
+        return ("/userstats/%s/" % user.username, 
+                "%s personal statistcs" % user.display_name)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/version.py.svn-base
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/version.py.svn-base b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/version.py.svn-base
new file mode 100644
index 0000000..6514373
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/.svn/text-base/version.py.svn-base
@@ -0,0 +1,2 @@
+__version_info__ = (0, 0)
+__version__ = '.'.join(map(str, __version_info__))

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

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/controllers/organizationstats.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/controllers/organizationstats.py b/ForgeOrganizationStats/forgeorganizationstats/controllers/organizationstats.py
new file mode 100644
index 0000000..6c3c590
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/controllers/organizationstats.py
@@ -0,0 +1,311 @@
+from pylons import c
+from tg import expose, validate, redirect
+from tg.decorators import with_trailing_slash
+from datetime import datetime, timedelta
+from allura.controllers import BaseController
+import allura.model as M
+from forgeorganization.organization.model import Organization
+from forgeorganizationstats.model import OrganizationStats
+from allura.lib.graphics.graphic_methods import create_histogram, create_progress_bar
+from forgeorganizationstats.widgets.forms import StatsPreferencesForm
+from allura.lib.decorators import require_post
+from allura.lib.security import require_access
+from allura.lib import validators as V
+
+stats_preferences_form = StatsPreferencesForm()
+
+class ForgeOrgStatsCatController(BaseController):
+    @expose()
+    def _lookup(self, category, *remainder):
+        cat = M.TroveCategory.query.get(shortname=category)
+        return ForgeOrgStatsCatController(self.organization, cat), remainder
+
+    def __init__(self, category=None):
+        self.category = category
+        super(ForgeOrgStatsCatController, self).__init__()
+
+    @expose('jinja:forgeorganizationstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return None
+        stats = self.organization.stats
+        if not stats: 
+            stats = OrganizationStats.create(self.organization)
+        if (not stats.visible) and (c.user.username not in c.project.admins()):
+            return dict(organization=self.organization)
+        
+        cat_id = None
+        if self.category: 
+            cat_id = self.category._id
+        ret_dict = _getDataForCategory(cat_id, self.organization.stats)
+        ret_dict['organization'] = self.organization
+        ret_dict['registration_date'] = stats.registration_date
+        ret_dict['category'] = self.category
+        
+        return ret_dict
+
+class ForgeOrgStatsController(BaseController):
+
+    category = ForgeOrgStatsCatController()
+
+    @expose('jinja:forgeorganizationstats:templates/index.html')
+    @with_trailing_slash
+    def index(self, **kw):
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return dict(organization=None)
+        stats = self.organization.stats
+        if not stats:
+            stats = OrganizationStats.create(self.organization)
+
+        if (not stats.visible) and (not (c.user.username in c.project.admins())):
+            return dict(organization=self.organization)
+
+        ret_dict = _getDataForCategory(None, stats)
+        ret_dict['organization'] = self.organization
+
+        ret_dict['registration_date'] = stats.registration_date
+
+        categories = {}
+        for el in self.organization.project_involvements:
+            if el.status == 'active':
+                p = el.project
+                for cat in p.trove_topic:
+                    cat = M.TroveCategory.query.get(_id = cat)
+                    if categories.get(cat):
+                        categories[cat] += 1
+                    else:
+                        categories[cat] = 1
+        categories = sorted(
+            categories.items(), 
+            key=lambda (x,y): y,
+            reverse=True)
+
+        ret_dict['maxcodecontrib'], ret_dict['averagecodecontrib'] =\
+            stats.getMaxAndAverageCodeContribution()
+        ret_dict['maxdisccontrib'], ret_dict['averagedisccontrib'] =\
+            stats.getMaxAndAverageDiscussionContribution()
+        ret_dict['maxticketcontrib'], ret_dict['averageticketcontrib'] =\
+            stats.getMaxAndAverageTicketsSolvingPercentage()
+        members = [m for m in self.organization.memberships 
+                   if m.status=='active']
+        now = datetime.utcnow()
+        newmembers = [m for m in self.organization.memberships
+                      if m.startdate and now - m.startdate < timedelta(30)]
+        leftmembers = [m for m in self.organization.memberships
+                       if m.closeddate and now - m.closeddate < timedelta(30)]
+        new_cooperations = [p for p in self.organization.project_involvements
+            if p.startdate and now - p.startdate < timedelta(30) and 
+               p.collaborationtype=='cooperation']
+        new_participations = [p for p in self.organization.project_involvements
+            if p.startdate and now - p.startdate < timedelta(30) and 
+               p.collaborationtype=='participation']
+        old_cooperations = [p for p in self.organization.project_involvements
+            if p.closeddate and now - p.closeddate < timedelta(30) and 
+               p.collaborationtype=='cooperation']
+        old_participations = [p for p in self.organization.project_involvements
+            if p.closeddate and now - p.closeddate < timedelta(30) and 
+               p.collaborationtype=='participation']
+
+        return dict(
+            ret_dict,
+            categories = categories,
+            codepercentage = stats.codeRanking(),
+            discussionpercentage = stats.discussionRanking(),
+            ticketspercentage = stats.ticketsRanking(),
+            codecontribution = stats.getCodeContribution(),
+            discussioncontribution = stats.getDiscussionContribution(),
+            ticketcontribution = stats.getTicketsContribution(),
+            membersnumber = len(members),
+            newmembers = len(newmembers),
+            leftmembers = len(leftmembers),
+            coopnumber=len(self.organization.getActiveCooperations()),
+            participnumber = len(self.organization.getActiveParticipations()),
+            newcooperations = len(new_cooperations),
+            newparticipations = len(new_participations),
+            oldcooperations = len(old_cooperations),
+            oldparticipations = len(old_participations),
+            permemberartifacts = stats.getLastMonthArtifactsPerMember(),
+            permembertickets = stats.getLastMonthTicketsPerMember(),
+            permembercommits = stats.getLastMonthCommitsPerMember())
+
+    @expose()
+    def categories_graph(self):
+        categories = {}
+        for el in self.organization.project_involvements:
+            if el.status == 'active':
+                p = el.project
+                for cat in p.trove_topic:
+                    cat = M.TroveCategory.query.get(_id = cat)
+                    if categories.get(cat):
+                        categories[cat] += 1
+                    else:
+                        categories[cat] = 1
+        data = []
+        labels = []
+        i = 0
+        for cat in sorted(categories.keys(), key=lambda x:x.fullname):
+            n = categories[cat]
+            data = data + [i] * n
+            label = cat.fullname
+            if len(label) > 15:
+                label = label[:15] + "..."
+            labels.append(label)
+            i += 1
+
+        return create_histogram(data, labels, 
+            'Number of projects', 'Projects by category')
+
+    @expose()
+    def code_ranking_bar(self):
+        return create_progress_bar(self.organization.stats.codeRanking())
+
+    @expose()
+    def discussion_ranking_bar(self):
+        return create_progress_bar(self.organization.stats.discussionRanking())
+
+    @expose()
+    def tickets_ranking_bar(self):
+        return create_progress_bar(self.organization.stats.ticketsRanking())
+
+    @expose('jinja:forgeorganizationstats:templates/commits.html')
+    @with_trailing_slash
+    def commits(self, **kw):
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return None
+        stats = self.organization.stats
+        if not stats: 
+            stats = OrganizationStats.create(self.organization)
+        if (not stats.visible) and (c.user.username not in c.project.admins()):
+            return dict(organization=self.organization)
+        
+        commits = stats.getCommitsByCategory()
+        return dict(organization = self.organization,
+                    data = commits) 
+
+    @expose('jinja:forgeorganizationstats:templates/artifacts.html')
+    @with_trailing_slash
+    def artifacts(self, **kw):
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return None
+        stats = self.organization.stats
+        if not stats: 
+            stats = OrganizationStats.create(self.organization)
+        if (not stats.visible) and (c.user.username not in c.project.admins()):
+            return dict(organization=self.organization)
+
+        artifacts = stats.getArtifactsByCategory(detailed=True)
+        return dict(organization = self.organization, data = artifacts) 
+
+    @expose('jinja:forgeorganizationstats:templates/tickets.html')
+    @with_trailing_slash
+    def tickets(self, **kw):
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return None
+        stats = self.organization.stats
+        if not stats: 
+            stats = OrganizationStats.create(self.organization)
+        if (not stats.visible) and (c.user.username not in c.project.admins()):
+            return dict(organization=self.organization)
+
+        artifacts = stats.getTicketsByCategory()
+        return dict(organization= self.organization, data = artifacts) 
+
+    @expose('jinja:forgeorganizationstats:templates/settings.html')
+    @with_trailing_slash
+    def settings(self, **kw):
+        require_access(c.project, 'admin')
+
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return dict(organization=None)
+        if not self.organization.stats:
+            OrganizationStats.create(self.organization)
+        return dict(
+            organization = self.organization, 
+            form = StatsPreferencesForm(
+                action = c.project.url() + 'organizationstats/change_settings'))
+
+    @expose()
+    @require_post()
+    @validate(stats_preferences_form, error_handler=settings)
+    def change_settings(self, **kw):
+        require_access(c.project, 'admin')
+
+        self.organization = c.project.organization_project_of
+        if not self.organization: 
+            return dict(organization=None)
+        if not self.organization.stats:
+            OrganizationStats.create(self.organization)
+        visible = kw.get('visible')
+        self.organization.stats.visible = visible
+        redirect(c.project.url() + 'organizationstats/settings')
+
+def _getDataForCategory(category, stats):
+    totcommits = stats.getCommits(category)
+    tottickets = stats.getTickets(category)
+    averagetime = tottickets.get('averagesolvingtime')
+    artifacts_by_type = stats.getArtifactsByType(category)
+    totartifacts = artifacts_by_type.get(None) 
+    if totartifacts:
+        del artifacts_by_type[None]
+    else:
+        totartifacts = dict(created=0, modified=0)
+    lmcommits = stats.getLastMonthCommits(category)
+    lm_artifacts_by_type = stats.getLastMonthArtifactsByType(category)
+    lm_totartifacts = stats.getLastMonthArtifacts(category)
+    lm_tickets = stats.getLastMonthTickets(category)
+
+    averagetime = lm_tickets.get('averagesolvingtime')
+
+    days = (datetime.utcnow() - stats.registration_date).days
+    if days >= 30: 
+        pmartifacts = dict(
+            created = round(totartifacts['created']*30.0/days,2),
+            modified=round(totartifacts['modified']*30.0/days,2))
+        pmcommits = dict(
+            number=round(totcommits['number']*30.0/days,2),
+            lines=round(totcommits['lines']*30.0/days,2))
+        pmtickets = dict(
+            assigned=round(tottickets['assigned']*30.0/days,2),
+            revoked=round(tottickets['revoked']*30.0/days,2),
+            solved=round(tottickets['solved']*30.0/days,2),
+            averagesolvingtime='n/a')
+        for key in artifacts_by_type:
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = \
+                round(value['created']*30.0/days,2)
+            artifacts_by_type[key]['pmmodified']= \
+                round(value['modified']*30.0/days,2)
+    else: 
+        pmartifacts = dict(created='n/a', modified='n/a')
+        pmcommits = dict(number='n/a', lines='n/a')
+        pmtickets = dict(
+            assigned='n/a',
+            revoked='n/a',
+            solved='n/a',
+            averagesolvingtime='n/a')
+        for key in artifacts_by_type:
+            value = artifacts_by_type[key]
+            artifacts_by_type[key]['pmcreated'] = 'n/a'
+            artifacts_by_type[key]['pmmodified']= 'n/a'
+
+    return dict(
+        days = days,
+        totcommits = totcommits,
+        lastmonthcommits = lmcommits,
+        lastmonthtickets = lm_tickets,
+        tottickets = tottickets,
+        permonthcommits = pmcommits,
+        totartifacts = totartifacts,
+        lastmonthartifacts = lm_totartifacts,
+        permonthartifacts = pmartifacts,
+        artifacts_by_type = artifacts_by_type,
+        lastmonth_artifacts_by_type = lm_artifacts_by_type,
+        permonthtickets = pmtickets)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/main.py b/ForgeOrganizationStats/forgeorganizationstats/main.py
new file mode 100644
index 0000000..5349964
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/main.py
@@ -0,0 +1,145 @@
+import logging
+from datetime import datetime
+
+from pylons import c
+from allura import model as M
+from allura.eventslistener import EventsListener
+from model import OrganizationStats
+from controllers.organizationstats import ForgeOrgStatsController
+from forgeorganizationstats import version
+from allura.app import Application, SitemapEntry
+from allura.lib import helpers as h
+
+log = logging.getLogger(__name__)
+
+class OrganizationStatsListener(EventsListener):
+    def newArtifact(self, art_type, art_datetime, project, user):
+        for org in _getInterestedOrganizations(user, project):
+            stats = org.stats
+            if not stats:
+                stats = OrganizationStats.create(org)
+            stats.addNewArtifact(art_type, art_datetime, project)
+
+    def modifiedArtifact(self, art_type, art_datetime, project, user):
+        for org in _getInterestedOrganizations(user, project):
+            stats = org.stats
+            if not stats:
+                stats = OrganizationStats.create(org)
+            stats.addModifiedArtifact(art_type, art_datetime,project)
+
+    def newOrganization(self, organization):
+        stats = OrganizationStats.create(organization)
+
+    def newUser(self, user):
+        pass
+
+    def ticketEvent(self, event_type, ticket, project, user):
+        if user is None:
+            return
+        organizations = _getInterestedOrganizations(user, project)
+        if event_type=="assigned":
+            for org in organizations:
+                stats = org.stats
+                if not stats:
+                    stats = OrganizationStats.create(org)
+                stats.addAssignedTicket(ticket.mod_date, project)
+        elif event_type=="revoked":
+            for org in organizations:
+                stats = org.stats
+                if not stats:
+                    stats = OrganizationStats.create(org)
+                stats.addRevokedTicket(ticket.mod_date, project)
+        elif event_type=="closed":
+            for org in organizations:
+                stats = org.stats
+                if not stats:
+                    stats = OrganizationStats.create(org)
+                stats.addClosedTicket(ticket.created_date, ticket.mod_date, project)
+
+    def newCommit(self, newcommit, project, user):
+        for org in _getInterestedOrganizations(user, project):
+            stats = org.stats
+            if not stats:
+                stats = OrganizationStats.create(org)
+            stats.addCommit(newcommit, datetime.utcnow(), project)
+
+    def addUserLogin(self, user):
+        pass
+
+def _getInterestedOrganizations(user, project):
+    proj_organizations=\
+        [org.organization for org in project.organizations
+         if org.status=='active']
+    return [m.organization for m in user.memberships
+            if m.status=='active' and m.organization in proj_organizations]
+
+class ForgeOrganizationStatsApp(Application):
+    __version__ = version.__version__
+    tool_label='Statistics'
+    default_mount_label='Statistics'
+    default_mount_point='organizationstats'
+    permissions = ['configure', 'read', 'write',
+                    'unmoderated_post', 'post', 'moderate', 'admin']
+    ordinal=15
+    installable=False
+    config_options = Application.config_options
+    default_external_feeds = []
+    icons={
+        24:'images/stats_24.png',
+        32:'images/stats_32.png',
+        48:'images/stats_48.png'
+    }
+    root = ForgeOrgStatsController()
+
+    def __init__(self, project, config):
+        Application.__init__(self, project, config)
+        role_admin = M.ProjectRole.by_name('Admin')._id
+        role_anon = M.ProjectRole.by_name('*anonymous')._id
+        self.config.acl = [
+            M.ACE.allow(role_anon, 'read'),
+            M.ACE.allow(role_admin, 'admin')]
+
+    def main_menu(self):
+        return [SitemapEntry(self.config.options.mount_label.title(), '.')]
+
+    @property
+    @h.exceptionless([], log)
+    def sitemap(self):
+        menu_id = self.config.options.mount_label.title()
+        with h.push_config(c, app=self):
+            return [
+                SitemapEntry(menu_id, '.')[self.sidebar_menu()] ]
+
+    @property
+    def show_discussion(self):
+        if 'show_discussion' in self.config.options:
+            return self.config.options['show_discussion']
+        else:
+            return True
+
+    @h.exceptionless([], log)
+    def sidebar_menu(self):
+        base = c.app.url
+        links = [SitemapEntry('Overview', base),
+                 SitemapEntry('Commits', base + 'commits'),
+                 SitemapEntry('Artifacts', base + 'artifacts'),
+                 SitemapEntry('Tickets', base + 'tickets')]
+        return links
+
+    def admin_menu(self):
+        links = [SitemapEntry(
+                     'Settings', c.project.url() + 'organizationstats/settings')]
+        return links
+
+    def install(self, project):
+        #It doesn't make any sense to install the tool twice on the same 
+        #project therefore, if it already exists, it doesn't install it
+        #a second time.
+        for tool in project.app_configs:
+            if tool.tool_name == 'organizationstats':
+                if self.config.options.mount_point!=tool.options.mount_point:
+                    project.uninstall_app(self.config.options.mount_point)
+                    return
+
+    def uninstall(self, project):
+        pass

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/model/.svn/all-wcprops
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/model/.svn/all-wcprops b/ForgeOrganizationStats/forgeorganizationstats/model/.svn/all-wcprops
new file mode 100644
index 0000000..a5d5661
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/model/.svn/all-wcprops
@@ -0,0 +1,17 @@
+K 25
+svn:wc:ra_dav:version-url
+V 58
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model
+END
+stats.py
+K 25
+svn:wc:ra_dav:version-url
+V 67
+/svn/allura/!svn/ver/3/ForgeUserStats/forgeuserstats/model/stats.py
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 70
+/svn/allura/!svn/ver/1/ForgeUserStats/forgeuserstats/model/__init__.py
+END

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganizationStats/forgeorganizationstats/model/.svn/entries
----------------------------------------------------------------------
diff --git a/ForgeOrganizationStats/forgeorganizationstats/model/.svn/entries b/ForgeOrganizationStats/forgeorganizationstats/model/.svn/entries
new file mode 100644
index 0000000..c26dfd9
--- /dev/null
+++ b/ForgeOrganizationStats/forgeorganizationstats/model/.svn/entries
@@ -0,0 +1,96 @@
+10
+
+dir
+4
+https://xp-dev.com/svn/allura/ForgeUserStats/forgeuserstats/model
+https://xp-dev.com/svn/allura
+
+
+
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+46ed536d-f66c-413e-a53e-834384f708db
+
+stats.py
+file
+
+
+
+
+2012-11-05T14:43:25.729756Z
+21591047edf4fabfb1b70150af5bd0c2
+2012-10-19T08:28:36.749162Z
+3
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+23647
+
+__init__.py
+file
+
+
+
+
+2012-11-05T14:43:25.729756Z
+d41d8cd98f00b204e9800998ecf8427e
+2012-10-17T19:55:53.450112Z
+1
+stefanoinvernizzi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+0
+

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


[4/5] git commit: organization and organization stats

Posted by st...@apache.org.
organization and organization stats

Signed-off-by: Stefano Invernizzi <st...@apache.org>


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

Branch: refs/heads/si/5566
Commit: 787ef235fe79b8bf0f51feb83ea92cfaf0a45b30
Parents: 2b3bad7
Author: Simone Gatti <si...@gmail.com>
Authored: Wed Apr 3 23:47:41 2013 +0200
Committer: Stefano Invernizzi <st...@apache.org>
Committed: Wed Apr 3 23:49:53 2013 +0200

----------------------------------------------------------------------
 Allura/allura/controllers/basetest_project_root.py |    4 +-
 Allura/allura/controllers/root.py                  |    4 +
 .../ext/user_profile/templates/user_index.html     |   22 +
 Allura/allura/lib/app_globals.py                   |    2 +
 Allura/allura/model/auth.py                        |   12 +
 Allura/allura/model/project.py                     |   19 +
 Allura/allura/nf/allura/css/allura.css             |   31 +-
 .../templates/jinja_master/theme_macros.html       |    5 +-
 Allura/allura/websetup/bootstrap.py                |   16 +
 Allura/development.ini                             |    1 +
 ForgeOrganization/forgeorganization/__init__.py    |    4 +
 .../organization/controller/organization.py        |  117 ++++
 .../forgeorganization/organization/main.py         |   44 ++
 .../organization/model/__init__.py                 |    1 +
 .../organization/model/organization.py             |  268 ++++++++
 .../organization/templates/register.html           |   25 +
 .../organization/templates/search_results.html     |   34 +
 .../organization/templates/user_memberships.html   |   51 ++
 .../organization/widgets/forms.py                  |  403 +++++++++++
 .../organization_profile/__init__.py               |    1 +
 .../organization_profile/organization_main.py      |  293 ++++++++
 .../templates/edit_profile.html                    |  111 +++
 .../templates/organization_index.html              |  206 ++++++
 .../forgeorganization/tests/test_organizations.py  |  396 +++++++++++
 .../forgeorganization/tool/controller/__init__.py  |    1 +
 .../tool/controller/organizationtool.py            |  144 ++++
 ForgeOrganization/forgeorganization/tool/main.py   |   91 +++
 .../forgeorganization/tool/templates/index.html    |  114 +++
 .../tool/templates/search_results.html             |   37 +
 .../forgeorganization/tool/widgets/forms.py        |  152 ++++
 ForgeOrganization/forgeorganization/version.py     |    2 +
 ForgeOrganization/setup.py                         |   33 +
 ForgeOrganization/test.ini                         |   57 ++
 ForgeOrganizationStats/.svn/all-wcprops            |   11 +
 ForgeOrganizationStats/.svn/entries                |   65 ++
 .../.svn/text-base/setup.py.svn-base               |   29 +
 .../forgeorganizationstats/.svn/all-wcprops        |   23 +
 .../forgeorganizationstats/.svn/entries            |  136 ++++
 .../.svn/text-base/__init__.py.svn-base            |    1 +
 .../.svn/text-base/main.py.svn-base                |  227 ++++++
 .../.svn/text-base/version.py.svn-base             |    2 +
 .../controllers/organizationstats.py               |  311 +++++++++
 .../forgeorganizationstats/main.py                 |  145 ++++
 .../forgeorganizationstats/model/.svn/all-wcprops  |   17 +
 .../forgeorganizationstats/model/.svn/entries      |   96 +++
 .../model/.svn/text-base/stats.py.svn-base         |  534 +++++++++++++++
 .../forgeorganizationstats/model/__init__.py       |    1 +
 .../forgeorganizationstats/model/orgstats.py       |   62 ++
 .../templates/.svn/all-wcprops                     |   29 +
 .../forgeorganizationstats/templates/.svn/entries  |  164 +++++
 .../.svn/text-base/artifacts.html.svn-base         |   48 ++
 .../templates/.svn/text-base/commits.html.svn-base |   37 +
 .../templates/.svn/text-base/index.html.svn-base   |  341 +++++++++
 .../templates/.svn/text-base/tickets.html.svn-base |   47 ++
 .../templates/artifacts.html                       |   67 ++
 .../forgeorganizationstats/templates/commits.html  |   56 ++
 .../forgeorganizationstats/templates/index.html    |  509 ++++++++++++++
 .../forgeorganizationstats/templates/settings.html |   19 +
 .../forgeorganizationstats/templates/tickets.html  |   66 ++
 .../forgeorganizationstats/tests/test_model.py     |  396 +++++++++++
 .../forgeorganizationstats/tests/test_stats.py     |  283 ++++++++
 .../forgeorganizationstats/version.py              |    2 +
 .../forgeorganizationstats/widgets/forms.py        |   22 +
 ForgeOrganizationStats/setup.py                    |   32 +
 ForgeOrganizationStats/test.ini                    |   56 ++
 65 files changed, 6530 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/controllers/basetest_project_root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/basetest_project_root.py b/Allura/allura/controllers/basetest_project_root.py
index 5626019..4a70465 100644
--- a/Allura/allura/controllers/basetest_project_root.py
+++ b/Allura/allura/controllers/basetest_project_root.py
@@ -6,7 +6,7 @@ from urllib import unquote
 
 import pkg_resources
 from pylons import tmpl_context as c
-from pylons import request, response
+from pylons import request, response, g
 from webob import exc
 from tg import expose
 from tg.decorators import without_trailing_slash
@@ -59,6 +59,8 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
         self.security = SecurityTests()
         for attr in ('index', 'browse', 'auth', 'nf', 'error'):
             setattr(self, attr, getattr(proxy_root, attr))
+        if g.show_organizations:
+            self.organization = proxy_root.organization
         self.gsearch = proxy_root.search
         self.rest = RestController()
         super(BasetestProjectRootController, self).__init__()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 83aa5e5..d286437 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -70,6 +70,10 @@ class RootController(WsgiDispatchController):
             n.bind_controller(self)
         self.browse = ProjectBrowseController()
 
+        if g.show_organizations:
+            ep = g.entry_points["organizations"].get('organization')
+            self.organization = ep().root
+
         super(RootController, self).__init__()
 
     def _setup_request(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/ext/user_profile/templates/user_index.html
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/templates/user_index.html b/Allura/allura/ext/user_profile/templates/user_index.html
index 2614953..4e0856e 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -236,6 +236,28 @@
     </div>
   </div>
 
+  {% if user.get_current_organizations() or user.get_past_organizations() %}
+    <div class="grid-24">
+      <div class="grid-24" style="margin:0;"><b>Organizations</b></div>
+      <div class="grid-24" style="margin-top:5px;margin-bottom:5px;">
+        <ul>
+          {% for org in user.get_current_organizations() %}
+            <li>
+              {{org.role.capitalize()}} at <a href="{{org.organization.url()}}">{{org.organization.fullname}}</a> 
+              from {{org.startdate.strftime('%d %B %Y')}}
+            </li>
+          {% endfor %}
+          {% for org in user.get_past_organizations() %}
+            <li>
+              {{org.role.capitalize()}} at <a href="{{org.organization.url()}}">{{org.organization.fullname}}</a> 
+              from {{org.startdate.strftime('%d %B %Y')}} to {{org.closeddate.strftime('%d %B %Y')}}
+            </li>
+          {% endfor %}
+        <ul>
+      </div>
+    </div>
+  {%endif%}
+
   {% if c.user.username == user.username %}
       <div class="address-list grid-18">
         <b>Email Addresses</b>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 1ab9b31..4faebcc 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -166,12 +166,14 @@ class Globals(object):
             theme=_cache_eps('allura.theme'),
             user_prefs=_cache_eps('allura.user_prefs'),
             spam=_cache_eps('allura.spam'),
+            organizations=_cache_eps('allura.organization'),
             stats=_cache_eps('allura.stats'),
             )
 
         # Zarkov logger
         self._zarkov = None
 
+        self.show_organizations = 'organization' in self.entry_points['organizations']
         # Set listeners to update stats
         statslisteners = []
         for name, ep in self.entry_points['stats'].iteritems():

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index a0bdb20..0769fd7 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -332,6 +332,8 @@ class User(MappedClass, ActivityNode, ActivityObject):
         level = S.OneOf('low', 'high', 'medium'),
         comment=str)])
 
+    #Organizations
+    memberships = RelationProperty('Membership')
     #Statistics
     stats_id = FieldProperty(S.ObjectId, if_missing=None)
 
@@ -355,6 +357,16 @@ class User(MappedClass, ActivityNode, ActivityObject):
     def set_pref(self, pref_name, pref_value):
         return plugin.UserPreferencesProvider.get().set_pref(self, pref_name, pref_value)
 
+    def get_current_organizations(self):
+        if hasattr(self, 'memberships'):
+            return [m for m in self.memberships if m.status=='active']
+        return []
+
+    def get_past_organizations(self):
+        if hasattr(self, 'memberships'):
+            return [m for m in self.memberships if m.status=='closed']
+        return []
+
     def add_socialnetwork(self, socialnetwork, accounturl):
         if socialnetwork == 'Twitter' and not accounturl.startswith('http'):
             accounturl = 'http://twitter.com/%s' % accounturl.replace('@', '')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index 4272345..4ad89ff 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -166,6 +166,8 @@ class Project(MappedClass, ActivityNode, ActivityObject):
     tracking_id = FieldProperty(str, if_missing='')
     is_nbhd_project=FieldProperty(bool, if_missing=False)
 
+    organizations=RelationProperty('ProjectInvolvement')
+
     # transient properties
     notifications_disabled = False
 
@@ -288,6 +290,10 @@ class Project(MappedClass, ActivityNode, ActivityObject):
     def is_user_project(self):
         return self.shortname.startswith('u/')
 
+    @property
+    def is_organization_project(self):
+        return self.shortname.startswith('o/')
+
     @LazyProperty
     def user_project_of(self):
         '''
@@ -299,6 +305,17 @@ class Project(MappedClass, ActivityNode, ActivityObject):
         return user
 
     @LazyProperty
+    def organization_project_of(self):
+        '''
+        If this is a organization-project, return the Organization, else None
+        '''
+        user = None
+        if self.is_organization_project:
+            from forgeorganization.organization.model import Organization
+            organization = Organization.query.get(shortname=self.shortname[2:])
+        return organization
+
+    @LazyProperty
     def root_project(self):
         if self.is_root: return self
         return self.parent_project.root_project
@@ -705,6 +722,8 @@ class Project(MappedClass, ActivityNode, ActivityObject):
                 apps = [('admin', 'admin', 'Admin'),
                         ('search', 'search', 'Search'),
                         ('activity', 'activity', 'Activity')]
+        if self.is_organization_project:
+            apps=[('organizationprofile', 'organizationprofile', 'Profile')]+apps
         with h.push_config(c, project=self, user=users[0]):
             # Install default named roles (#78)
             root_project_id=self.root_project._id

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/nf/allura/css/allura.css
----------------------------------------------------------------------
diff --git a/Allura/allura/nf/allura/css/allura.css b/Allura/allura/nf/allura/css/allura.css
index 613caa7..fd0f0da 100644
--- a/Allura/allura/nf/allura/css/allura.css
+++ b/Allura/allura/nf/allura/css/allura.css
@@ -41,6 +41,11 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-repeat: no-repeat;
 }
 
+.ui-icon-tool-organizationprofile {
+  background-image: url("../images/home_24.png");
+  background-repeat: no-repeat;
+}
+
 .ui-icon-tool-wiki {
   background-image: url("../images/wiki_24.png");
   background-repeat: no-repeat;
@@ -60,7 +65,7 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-repeat: no-repeat;
 }
 
-.ui-icon-tool-userstats {
+.ui-icon-tool-userstats, .ui-icon-tool-organizationstats{
   background-image: url("../images/stats_24.png");
   background-repeat: no-repeat;
 }
@@ -96,6 +101,11 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-repeat: no-repeat;
 }
 
+.ui-icon-tool-organizationstool {
+  background-image: url("../images/org_24.png");
+  background-repeat: no-repeat;
+}
+
 .ui-icon-tool-chat {
   background-image: url("../images/chat_24.png");
   background-repeat: no-repeat;
@@ -120,10 +130,15 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 {
   background-image: url("../images/code_32.png");
 }
+
+.ui-icon-tool-organizationprofile{
+  background-image: url("../images/home_32.png");
+  background-repeat: no-repeat;
+}
 #top_nav .ui-icon-tool-stats {
   background-image: url("../images/stats_32.png");
 }
-#top_nav .ui-icon-tool-userstats {
+#top_nav .ui-icon-tool-userstats, .ui-icon-tool-organizationstats {
   background-image: url("../images/stats_32.png");
 }
 
@@ -149,9 +164,19 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
   background-image: url("../images/chat_32.png");
 }
 
+#top_nav .ui-icon-tool-organizationstool {
+  background-image: url("../images/org_32.png");
+}
+.big_icon.ui-icon-tool-organizationstool {
+  background-image: url("../images/org_48.png");
+}
+
 .big_icon.ui-icon-tool-home, .big_icon.ui-icon-tool-profile {
   background-image: url("../images/home_48.png");
 }
+.big_icon.ui-icon-tool-organizationprofile {
+  background-image: url("../images/home_48.png");
+}
 .big_icon.ui-icon-tool-wiki {
   background-image: url("../images/wiki_48.png");
 }
@@ -161,7 +186,7 @@ b.ico.ico-vote-down { background-image: url('../images/vote_down.png'); }
 .big_icon.ui-icon-tool-stats {
   background-image: url("../images/stats_48.png");
 }
-.big_icon.ui-icon-tool-userstats {
+.big_icon.ui-icon-tool-userstats, .big_icon.ui-icon-tool-organizationstats {
   background-image: url("../images/stats_48.png");
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/templates/jinja_master/theme_macros.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/jinja_master/theme_macros.html b/Allura/allura/templates/jinja_master/theme_macros.html
index d0d28ed..0a27187 100644
--- a/Allura/allura/templates/jinja_master/theme_macros.html
+++ b/Allura/allura/templates/jinja_master/theme_macros.html
@@ -3,7 +3,10 @@
     <div class="wrapper">
         <nav>
           {% if c.user._id %}
-            <a href="/auth/preferences/">Account</a>
+            {%if g.show_organizations %}
+              <a href="/organization/">Organizations</a>
+            {% endif %}
+            <a href="/auth/prefs/">Account</a>
             <a href="{{c.user.url()}}">{{name}}</a>
             <a href="{{logout_url}}">Log Out</a>
           {% else %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/allura/websetup/bootstrap.py
----------------------------------------------------------------------
diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py
index 90c8e62..1809fef 100644
--- a/Allura/allura/websetup/bootstrap.py
+++ b/Allura/allura/websetup/bootstrap.py
@@ -6,6 +6,7 @@ import logging
 import shutil
 from collections import defaultdict
 from datetime import datetime
+import pkg_resources
 
 import tg
 from pylons import tmpl_context as c, app_globals as g
@@ -82,6 +83,7 @@ def bootstrap(command, conf, vars):
     root = create_user('Root', make_project=False)
 
     n_projects = M.Neighborhood(name='Projects', url_prefix='/p/',
+                             anchored_tools='admin:Admin,organizationstool:Organizations',
                                 features=dict(private_projects = True,
                                               max_projects = None,
                                               css = 'none',
@@ -93,7 +95,15 @@ def bootstrap(command, conf, vars):
                                            max_projects = None,
                                            css = 'none',
                                            google_analytics = False))
+    n_organizations = M.Neighborhood(name='Organizations', url_prefix='/o/',
+                             shortname_prefix='o/',
+                             anchored_tools='organizationprofile:Profile,organizationstats:Statistics',
+                             features=dict(private_projects = True,
+                                           max_projects = None,
+                                           css = 'none',
+                                           google_analytics = False))
     n_adobe = M.Neighborhood(name='Adobe', url_prefix='/adobe/', project_list_url='/adobe/',
+                             anchored_tools='admin:Admin,organizationstool:Organizations',
                              features=dict(private_projects = True,
                                            max_projects = None,
                                            css = 'custom',
@@ -103,6 +113,7 @@ def bootstrap(command, conf, vars):
     p_projects = project_reg.register_neighborhood_project(n_projects, [root], allow_register=True)
     p_users = project_reg.register_neighborhood_project(n_users, [root])
     p_adobe = project_reg.register_neighborhood_project(n_adobe, [root])
+    p_organizations = project_reg.register_neighborhood_project(n_organizations, [root])
     ThreadLocalORMSession.flush_all()
     ThreadLocalORMSession.close_all()
 
@@ -176,6 +187,11 @@ def bootstrap(command, conf, vars):
     ThreadLocalORMSession.flush_all()
     ThreadLocalORMSession.close_all()
 
+    ep = pkg_resources.get_entry_info(
+        'forgeorganization', 'allura.organization', 'organization')
+    if ep is not None: 
+        ep.load().bootstrap()
+
 def wipe_database():
     conn = M.main_doc_session.bind.conn
     create_trove_categories = CreateTroveCategoriesCommand('create_trove_categories')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 3dc76d0..b5dbbda 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -125,6 +125,7 @@ scm.repos.tarball.root = /usr/share/nginx/www/
 scm.repos.tarball.url_prefix = http://localhost/
 
 trovecategories.enableediting = true
+organizations.enable = true
 
 # ActivityStream
 activitystream.master = mongodb://127.0.0.1:27017

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/__init__.py b/ForgeOrganization/forgeorganization/__init__.py
new file mode 100644
index 0000000..c10c2db
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/__init__.py
@@ -0,0 +1,4 @@
+import organization
+import tool
+from organization.main import ForgeOrganizationApp
+from tool.main import ForgeOrganizationToolApp

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

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/controller/organization.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/controller/organization.py b/ForgeOrganization/forgeorganization/organization/controller/organization.py
new file mode 100644
index 0000000..1e2bada
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/controller/organization.py
@@ -0,0 +1,117 @@
+from tg import expose, flash, redirect, validate
+from tg.decorators import with_trailing_slash
+import pylons
+c = pylons.c = pylons.tmpl_context
+g = pylons.g = pylons.app_globals
+import re
+
+from allura.lib import validators as V
+from allura.lib.security import require_authenticated
+from allura.lib.decorators import require_post
+import allura.model as M
+from allura.controllers import BaseController
+import forgeorganization.organization.widgets.forms as forms
+from forgeorganization.organization.model import Organization, Membership
+from forgeorganization.organization.model import WorkFields, ProjectInvolvement
+from datetime import datetime
+from pkg_resources import get_entry_info
+
+class Forms(object):
+    registration_form = forms.RegistrationForm(action='/organization/save_new')
+    search_form = forms.SearchForm(action='/organization/search')
+    admission_request_form = forms.RequestAdmissionForm()
+    request_collaboration_form = forms.RequestCollaborationForm()
+
+    def new_change_collaboration_status(self):
+        return forms.ChangeCollaborationStatusForm()
+
+    def new_change_membership_from_user_form(self):
+        return forms.ChangeMembershipFromUser()
+    
+    def new_change_membership_from_organization(self):
+        return forms.ChangeMembershipFromOrganization()
+
+F = Forms()
+
+class OrganizationController(object):
+    @expose('jinja:forgeorganization:organization/templates/user_memberships.html')
+    def index(self, **kw):
+        require_authenticated()
+
+        return dict(
+            memberships=[m for m in c.user.memberships if m.status!='closed'],
+            forms=F)
+
+    @expose('jinja:forgeorganization:organization/templates/search_results.html')
+    @require_post()
+    @validate(F.search_form, error_handler=index)
+    def search(self, orgname, **kw):
+        regx = re.compile(orgname, re.IGNORECASE)
+        orgs = Organization.query.find(dict(fullname=regx))
+        return dict(
+            orglist = orgs, 
+            forms = F, 
+            search_string = orgname)
+
+    @expose('jinja:forgeorganization:organization/templates/register.html')
+    def register(self, **kw):
+        require_authenticated()
+        return dict(forms=F)
+
+    @expose()
+    @require_post()
+    @validate(F.registration_form, error_handler=register)
+    def save_new(self, fullname, shortname, orgtype, role, **kw):
+        o = Organization.register(shortname, fullname, orgtype, c.user)
+        if o is None: 
+            flash(
+                'The short name "%s" has been taken by another organization.' \
+                % shortname, 'error')
+            redirect('/organization/register')
+        m = Membership.insert(role, 'active', o._id, c.user._id)
+        flash('Organization "%s" correctly created!' % fullname)
+        redirect('%sadmin/organizationprofile' % o.url())
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def change_membership(self, **kw):        
+        membershipid = kw['membershipid']
+        memb = Membership.getById(membershipid)
+        status = kw['status']
+
+        return_url = '/organization'
+
+        if c.user != memb.member:
+            flash(
+                "You don't have the permission to perform this action.", 
+                "error")
+            redirect(return_url)
+
+        if status == 'remove':
+            old_status = memb.status
+            if memb.status in ['invitation', 'request']:
+                Membership.delete(memb)
+                flash('The pending %s has been removed.' % old_status)
+                redirect(return_url)
+                return
+            else:
+                flash(
+                    "You don't have the permission to perform this action.", 
+                    "error")
+                redirect(return_url)
+                return
+
+        allowed=True
+        if memb.status=='closed' and status!='closed':
+            allowed=False
+        if memb.status=='request' and status=='active':
+            allowed=False
+
+        if allowed:
+            memb.setStatus(status)
+            memb.role = kw.get('role')
+            flash('The membership has been successfully updated.')
+        else:
+            flash("You are not allowed to perform this action.")
+        redirect(return_url)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/main.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/main.py b/ForgeOrganization/forgeorganization/organization/main.py
new file mode 100644
index 0000000..94c561e
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/main.py
@@ -0,0 +1,44 @@
+import logging
+import allura.model as M
+
+from model.organization import WorkFields
+from controller.organization import OrganizationController
+
+log = logging.getLogger(__name__)
+
+class ForgeOrganizationApp:
+    root = OrganizationController()
+    
+    @classmethod
+    def bootstrap(self) :
+        conn = M.main_doc_session.bind.conn
+        if 'allura' in conn.database_names():
+            db = conn['allura']
+            if 'work_fields' in db.collection_names():
+                log.info('Dropping collection allura.work_fields')
+                db.drop_collection('work_fields')
+
+        l = [
+            ('Home & Entertainment',
+             'Applications designed primarily for use in or for the home, '+\
+             'or for entertainment.'),
+            ('Content & Communication',
+             'Office productivity suites, multimedia players, file viewers, '+\
+             'Web browsers, collaboration tools, ...'),
+            ('Education & Reference',
+             'Educational software, learning support tools, ...'),
+            ('Operations & Professionals',
+             'ERPs, CRMs, SCMs, applications for specific business uses, ...'),
+            ('Product manufacturing and service delivery',
+             'Software to support specific product manufacturing and '+\
+             'service delivery'),
+            ('Platform & Management',
+             'Operating systems, security, infrastructure services, ' + \
+             'hardware components controllers, ...'),
+            ('Mobile apps',
+             'Applications for mobile devices, such as telephones, PDAs, ...'),
+            ('Web applications','Applications available on the web')]
+
+        for (n, d) in l: 
+            log.info('Added work field %s.' % n)
+            WorkFields.insert(n,d)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/model/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/model/__init__.py b/ForgeOrganization/forgeorganization/organization/model/__init__.py
new file mode 100644
index 0000000..6cf34c0
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/model/__init__.py
@@ -0,0 +1 @@
+from organization import Organization, Membership, WorkFields, ProjectInvolvement

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/model/organization.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/model/organization.py b/ForgeOrganization/forgeorganization/organization/model/organization.py
new file mode 100644
index 0000000..32e6316
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/model/organization.py
@@ -0,0 +1,268 @@
+from datetime import datetime
+
+import iso8601
+import pymongo
+import pylons
+pylons.c = pylons.tmpl_context
+pylons.g = pylons.app_globals
+from pylons import c, g
+
+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
+
+import allura.tasks.mail_tasks
+from allura.lib import helpers as h
+from allura.lib import plugin
+
+from allura.model.session import main_orm_session
+
+from allura.model import User
+import allura.model as M
+
+class Organization(MappedClass):
+    class __mongometa__:
+        name='organization'
+        session = main_orm_session
+        unique_indexes = [ 'shortname' ]
+
+    _id=FieldProperty(S.ObjectId)
+    shortname=FieldProperty(str)
+    fullname=FieldProperty(str)
+    organization_type=FieldProperty(S.OneOf(
+        'For-profit business',
+        'Foundation or other non-profit organization',
+        'Research and/or education institution'))
+    description=FieldProperty(str)
+    headquarters=FieldProperty(str)
+    dimension=FieldProperty(
+        S.OneOf('Small', 'Medium', 'Large', 'Unknown'),
+        if_missing = 'Unknown')
+    website=FieldProperty(str)
+    workfields=FieldProperty([S.ObjectId])
+    created=FieldProperty(S.DateTime, if_missing=datetime.utcnow())
+    
+    memberships=RelationProperty('Membership')
+    project_involvements=RelationProperty('ProjectInvolvement')
+
+    stats_id = FieldProperty(S.ObjectId, if_missing=None)
+
+    @property
+    def stats(self):
+        if 'organizationstats' in g.entry_points['stats']:
+            from forgeorganizationstats.model import OrganizationStats
+            return OrganizationStats.query.get(_id=self.stats_id)
+        else:
+            return None
+
+    def url(self):
+        return ('/o/' + self.shortname.replace('_', '-') + '/').encode('ascii','ignore')
+
+    def project(self):
+        return M.Project.query.get(
+            shortname='o/'+self.shortname.replace('_', '-'))
+
+    @classmethod
+    def register(cls, shortname, fullname, orgtype, user):
+        o=cls.query.get(shortname=shortname)
+        if o is not None: return None
+        try:
+            o = cls(
+                shortname=shortname, 
+                fullname=fullname,
+                organization_type=orgtype)
+            session(o).flush(o)
+        except pymongo.errors.DuplicateKeyError:
+            session(o).expunge(o)
+            return None
+        if o is not None:
+            n = M.Neighborhood.query.get(name='Organizations')
+            n.register_project('o/'+shortname, user=user, user_project=False)
+            g.statsUpdater.newOrganization(o)
+        return o
+
+    @classmethod
+    def delete(cls, o):
+        try:
+            session(o).expunge(o)
+        except:
+            return False
+        return True
+
+    @classmethod
+    def getById(cls, org_id):
+        org_id = str(org_id)
+        org_id = bson.ObjectId(org_id)
+        return cls.query.get(_id=org_id)
+
+    def getWorkfields(self):
+        l = []
+        for wf in self.workfields:
+            l.append(WorkFields.query.get(_id = wf))
+        return l
+
+    def addWorkField(self, workfield):
+        wfid = workfield._id
+        if not wfid in self.workfields:
+            self.workfields.append(wfid)
+           
+    def removeWorkField(self, workfield):
+        wfid = workfield._id
+        if wfid in self.workfields:
+            del self.workfields[self.workfields.index(wfid)]
+
+    def getActiveCooperations(self):
+        return [c for c in self.project_involvements if c.status=='active' and
+            c.collaborationtype == 'cooperation']
+
+    def getPastCooperations(self):
+        return [c for c in self.project_involvements if c.status=='closed' and 
+            c.collaborationtype == 'cooperation']
+
+    def getActiveParticipations(self):
+        return [c for c in self.project_involvements if c.status=='active' and 
+            c.collaborationtype == 'participation']
+
+    def getPastParticipations(self):
+        return [c for c in self.project_involvements if c.status=='closed' and 
+            c.collaborationtype == 'participation']
+
+    def getEnrolledUsers(self):
+        return [m for m in self.memberships if m.status=='active']
+
+class WorkFields(MappedClass):
+    class __mongometa__:
+        session = main_orm_session
+        name='work_fields'
+
+    _id=FieldProperty(S.ObjectId)
+    name=FieldProperty(str)
+    description=FieldProperty(str, if_missing='')
+
+    @classmethod
+    def insert(cls, name, description):
+        wf=cls.query.get(name=name)
+        if wf is not None: 
+            return None
+        try:
+            wf = cls(name=name, description=description)
+            session(wf).flush(wf)
+        except pymongo.errors.DuplicateKeyError:
+            session(wf).expunge(wf)
+            return None
+        return wf
+
+    @classmethod
+    def getById(cls, workfieldid):
+        workfieldid = str(workfieldid)
+        workfieldid = bson.ObjectId(workfieldid)
+        return cls.query.get(_id=workfieldid)
+
+class Membership(MappedClass):
+    class __mongometa__:
+        session = main_orm_session
+        name='organization_membership'
+
+    _id=FieldProperty(S.ObjectId)
+    status=FieldProperty(S.OneOf('active', 'closed', 'invitation', 'request'))
+    role=FieldProperty(str)
+    organization_id=ForeignIdProperty('Organization')
+    member_id=ForeignIdProperty('User')
+    startdate = FieldProperty(S.DateTime, if_missing=None)
+    closeddate = FieldProperty(S.DateTime, if_missing=None)
+
+    organization = RelationProperty('Organization')
+    member = RelationProperty('User')
+
+    @classmethod
+    def insert(cls, role, status, organization_id, member_id):
+        m = cls.query.find(dict(organization_id=organization_id, member_id=member_id))
+        for el in m:
+            if el.status!='closed':
+                return None
+        try:
+            m = cls(
+                organization_id=organization_id, 
+                member_id=member_id,
+                role=role,
+                startdate=None,
+                status=status)
+            session(m).flush(m)
+        except pymongo.errors.DuplicateKeyError:
+            session(m).expunge(m)
+            m = cls.query.get(organization_id=organization_id, member_id=member_id)
+        if status == 'active':
+            m.startdate = datetime.utcnow()
+
+        return m
+    
+    @classmethod
+    def delete(cls, membership):
+        cls.query.remove(dict(_id=membership._id))
+        
+    @classmethod
+    def getById(cls, membershipid):
+        membershipid = str(membershipid)
+        membershipid = bson.ObjectId(membershipid)
+        return cls.query.get(_id=membershipid)
+
+    def setStatus(self, status):
+        if status=='active' and self.status!='active':
+            self.startdate = datetime.utcnow()
+        elif status=='closed':
+            self.closeddate = datetime.utcnow()
+        self.status = status
+
+class ProjectInvolvement(MappedClass):
+    class __mongometa__:
+        session = main_orm_session
+        name='project_involvement'
+
+    _id=FieldProperty(S.ObjectId)
+    status=FieldProperty(S.OneOf('active', 'closed', 'invitation', 'request'))
+    collaborationtype=FieldProperty(S.OneOf('cooperation', 'participation'))
+    organization_id=ForeignIdProperty('Organization')
+    project_id=ForeignIdProperty('Project')
+    startdate = FieldProperty(S.DateTime, if_missing=None)
+    closeddate = FieldProperty(S.DateTime, if_missing=None)
+
+    organization = RelationProperty('Organization')
+    project = RelationProperty('Project')
+    
+    @classmethod
+    def insert(cls, status, collaborationtype, organization_id, project_id):
+        p = cls.query.find(dict(
+            organization_id=organization_id, 
+            project_id=project_id))
+        for el in p:
+            if p.status != 'closed':
+                return None
+        try:
+            m = cls(organization_id=organization_id, project_id=project_id, status=status, collaborationtype=collaborationtype)
+            session(m).flush(m)
+        except pymongo.errors.DuplicateKeyError:
+            session(m).expunge(m)
+            m = cls.query.get(organization_id=organization_id, project_id=project_id)
+        return m
+    
+    @classmethod
+    def delete(cls, coll_id):
+        cls.query.remove(dict(_id=coll_id))
+
+    @classmethod
+    def getById(cls, p_id):
+        p_id = str(p_id)
+        p_id = bson.ObjectId(p_id)
+        return cls.query.get(_id=p_id)
+
+    def setStatus(self, status):
+        if status=='active' and self.status!='active':
+            self.startdate = datetime.utcnow()
+        elif status=='closed':
+            self.closeddate = datetime.utcnow()
+        self.status = status
+
+Mapper.compile_all()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/templates/register.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/templates/register.html b/ForgeOrganization/forgeorganization/organization/templates/register.html
new file mode 100644
index 0000000..461f962
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/templates/register.html
@@ -0,0 +1,25 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Create organization{% endblock %}
+
+{% block header %}Create organization{% endblock %}
+
+{% block content %}
+
+<div class="grid-20">
+  <h2>Create a new organization profile</h2>
+  <p>
+    If you want to create a new organization, fill the form below, then press "Save".
+    Please, note that "Your Role" should be filled with the role you have in the real life within the organization 
+    (for example, C.E.O., developer, ...).
+    In the field "Desired Short Name", put a valid unique name which will be used to distinguish your organization from
+    the other ones.
+  </p>
+
+  {{ forms.registration_form.display() }}
+</div>
+
+{% endblock %}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/templates/search_results.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/templates/search_results.html b/ForgeOrganization/forgeorganization/organization/templates/search_results.html
new file mode 100644
index 0000000..59f33ce
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/templates/search_results.html
@@ -0,0 +1,34 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Search results{% endblock %}
+
+{% block header %}Search results{% endblock %}
+
+{% block content %}
+
+  <div class="grid-20">
+    <h2>Results</h2>
+    {% if not orglist %}
+      <p>
+        Your entered the search string "{{search_string}}", but there isn't any organization
+        matching your query. Try to change your search string.
+      </p>
+    {% else %}
+      <p>
+        Your search for "{{search_string}}" produced {{orglist|length}} result{% if orglist|length != 1 %}s{% endif %}: 
+      </p>
+      <ul>
+        {% for o in orglist %}
+          <li><a href="{{o.url()}}">{{o.fullname}}</a></li>
+        {% endfor %}
+      </ul>
+    {% endif %}
+  </div>
+  <div class="grid-20">
+    <h2>Perform a new search</h2>
+    {{forms.search_form.display()}}
+  </div>
+
+{% endblock %}
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/templates/user_memberships.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/templates/user_memberships.html b/ForgeOrganization/forgeorganization/organization/templates/user_memberships.html
new file mode 100644
index 0000000..f130cea
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/templates/user_memberships.html
@@ -0,0 +1,51 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+{% block title %}{{c.user.display_name}}'s organizations{% endblock %}
+{% block header %}{{c.user.display_name}}'s organizations{% endblock %}
+{% block content %}
+ 
+  <div class="grid-20">
+    <h2>Your organizations</h2>
+    {% if memberships %}
+      <table>
+        <thead>
+          <tr>
+            <th>Name</th>
+            <th>Organization type</th>
+            <th>Role</th>
+            <th>Status</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for membership in memberships %}
+            {{forms.new_change_membership_from_user_form().display(
+                  membership=membership,
+                  action='organization/change_membership')}}
+          {% endfor %}
+        </tbody>
+      </table>
+    {% else %}
+      <p>
+        At the moment, it looks like you are not involved in any organization. 
+      </p>
+    {% endif %}
+  </div>
+
+  <div class="grid-20">
+    <h2>Add your enrollment with an organization</h2>
+    <p>If you are a member of an organization which is not included in your list, you can simply add it.</p>
+    <h3>Already existing organizations</h3>
+    <p>
+      If your organization already has a profile on the forge, you can search for it typing the organization's name 
+      in the form below. Then, you simply have to ask to be added to the member of the organization. 
+      {{forms.search_form.display()}}
+    </p>
+    <h3>New organizations</h3>
+    <p>
+      If your organization doesn't exist on the forge, you can create its profile. <a href="/organization/register">Click here</a> 
+      to do it.
+    </p>
+  </div>
+
+{% endblock %}

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization/widgets/forms.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization/widgets/forms.py b/ForgeOrganization/forgeorganization/organization/widgets/forms.py
new file mode 100644
index 0000000..f0403e1
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization/widgets/forms.py
@@ -0,0 +1,403 @@
+import logging
+import warnings
+from pylons import g, c
+from allura.lib import validators as V
+from allura.lib import helpers as h
+from allura.lib import plugin
+from allura.lib.widgets import form_fields as ffw
+from allura.lib.widgets.forms import ForgeForm
+from allura import model as M
+
+from formencode import validators as fev
+import formencode
+from forgeorganization.organization.model import WorkFields
+import ew as ew_core
+import ew.jinja2_ew as ew
+
+from pytz import common_timezones, country_timezones, country_names
+
+log = logging.getLogger(__name__)
+
+class RegistrationForm(ForgeForm):
+    class fields(ew_core.NameList):
+        fullname = ew.TextField(
+            label='Organization Full Name',
+            validator=fev.UnicodeString(not_empty=True))
+        shortname = ew.TextField(
+            label='Desired Short Name',
+            validator=formencode.All(
+                fev.Regex(h.re_path_portion),
+                fev.UnicodeString(not_empty=True)))
+        orgtype = ew.SingleSelectField(
+            label='Organization Type',
+            options = [
+                ew.Option(
+                    py_value='For-profit business', 
+                    label='For-profit business'),
+                ew.Option(
+                    py_value='Foundation or other non-profit organization',
+                    label='Foundation or other non-profit organization'),
+                ew.Option(
+                    py_value='Research and/or education institution',
+                    label='Research and/or education institution')],
+            validator=fev.UnicodeString(not_empty=True))
+        role = ew.TextField(
+            label='Your Role',
+            validator=fev.UnicodeString(not_empty=True))
+
+class SearchForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text='Search')
+
+    class fields(ew_core.NameList):
+        orgname = ew.TextField(
+            label='Organization name', 
+            validator=fev.UnicodeString(not_empty=True))
+
+class RequestAdmissionForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        role = ew.TextField(
+            label='Your role', 
+            validator=fev.UnicodeString(not_empty=True))
+
+class RequestCollaborationForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        project_url_name = ew.TextField(
+            label='Project URL Name', 
+            validator=fev.UnicodeString(not_empty=True))
+        collaboration_type=ew.SingleSelectField(
+            label='Collaboration Type', 
+            options = [
+                ew.Option(py_value='cooperation', label='Cooperation'),
+                ew.Option(py_value='participation', label='Participation')])
+
+class UpdateProfile(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        fullname=ew.TextField(
+            label='Organization Full Name',
+            validator=fev.UnicodeString(not_empty=True))
+        organization_type=ew.SingleSelectField(
+            label='Organization Type', 
+            options = [
+                ew.Option(
+                    py_value='For-profit business', 
+                    label='For-profit business'),
+                ew.Option(
+                    py_value='Foundation or other non-profit organization',
+                    label='Foundation or other non-profit organization'),
+                ew.Option(
+                    py_value='Research and/or education institution',
+                    label='Research and/or education institution')],
+             validator=fev.UnicodeString(not_empty=True))
+        description=ew.TextField(
+            label='Description')
+        dimension=ew.SingleSelectField(
+            label='Dimension', 
+            options = [
+                ew.Option(
+                    py_value='Small', 
+                    label='Small organization (up to 50 members)'),
+                ew.Option(
+                    py_value='Medium',
+                    label='Medium-size organization (51-250 members)'),
+                ew.Option(
+                    py_value='Large',
+                    label='Big organization (at least 251 members)'),
+                ew.Option(
+                    py_value='Unknown',
+                    label='Unknown')],
+            validator=fev.UnicodeString(not_empty=True))
+        headquarters=ew.TextField(
+            label='Headquarters')
+        website=ew.TextField(
+            label='Website')
+
+    def display(self, **kw):
+        organization = kw.get('organization')
+        self.fields['fullname'].attrs = dict(value=organization.fullname)
+        self.fields['description'].attrs = dict(value=organization.description)
+        for opt in self.fields['organization_type'].options:
+            if opt.py_value == organization.organization_type:
+                opt.selected = True
+            else:
+                opt.selected = False
+        for opt in self.fields['dimension'].options:
+            if opt.py_value == organization.dimension:
+                opt.selected = True
+            else:
+                opt.selected = False
+        self.fields['website'].attrs = dict(value=organization.website)
+        self.fields['headquarters'].attrs = \
+            dict(value=organization.headquarters)
+
+        return super(UpdateProfile, self).display(**kw)
+
+class InviteUser(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        username=ew.TextField(
+            label='Username',
+            validator=fev.UnicodeString(not_empty=True))
+        role=ew.TextField(
+            label='Role',
+            validator=fev.UnicodeString(not_empty=True))
+        
+class AddWorkField(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    def display(self, **kw):
+        self.fields = [
+            ew.SingleSelectField(
+                name='workfield',
+                label='Work Field',
+                options = [ew.Option(py_value=wf._id, label=wf.name)
+                           for wf in WorkFields.query.find()],
+                validator=fev.UnicodeString(not_empty=True))]
+        return super(AddWorkField, self).display(**kw)
+
+
+    def to_python(self, value, state):
+        d = super(AddWorkField, self).to_python(value, state)
+        return d
+
+class RemoveWorkField(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        wf = kw.get('workfield')
+
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="workfieldid",
+                        attrs={'value':str(wf._id)},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=wf.name,
+                        show_errors=False),
+                    ew.HTMLField(
+                        text=wf.description,
+                        show_errors=False),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Remove'},
+                        show_errors=False)])]
+        return super(RemoveWorkField, self).display(**kw)
+
+    def to_python(self, value, state):
+        d = {}
+        d['workfield'] = WorkFields.getById(value['workfieldid'])
+        return d
+
+class ChangeMembershipFromUser(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        m = kw.get('membership')
+        org = m.organization
+
+        orgnamefield = '<a href="%s">%s</a>' % (org.url()+"organizationprofile", org.fullname)
+        if c.user.username in m.organization.project().admins():
+            orgnamefield+=' (<a href="%sadmin/organizationprofile">edit</a>)'%org.url()
+        if m.status == 'active':
+            statusoptions = [
+                ew.Option(py_value='active',label='Active',selected=True),
+                ew.Option(py_value='closed',label='Closed',selected=False)]
+        elif m.status == 'closed':
+            statusoptions = [
+                ew.Option(py_value='closed',label='Closed',selected=True)]
+        elif m.status == 'invitation':
+            statusoptions = [
+                ew.Option(
+                    py_value='invitation',
+                    label='Pending invitation',
+                    selected=True),
+                ew.Option(py_value='active',label='Accept',selected=False),
+                ew.Option(py_value='remove',label='Decline',selected=False)]
+        elif m.status == 'request':
+            statusoptions = [
+                ew.Option(
+                    py_value='request',label='Pending request',selected=True),
+                ew.Option(
+                    py_value='remove',label='Remove request',selected=False)]
+ 
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="membershipid",
+                        attrs={'value':str(m._id)},
+                        show_errors=False),
+                    ew.HiddenField(
+                        name="requestfrom",
+                        attrs={'value':'user'},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=orgnamefield,
+                        show_errors=False),
+                    ew.HTMLField(
+                        text=org.organization_type,
+                        show_errors=False),
+                    ew.TextField(
+                        name='role',
+                        attrs=dict(value=m.role),
+                        show_errors=False),
+                    ew.SingleSelectField(
+                        name='status',
+                        show_errors=False,
+                        options = statusoptions),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Save'},
+                        show_errors=False)])]
+        return super(ChangeMembershipFromUser, self).display(**kw)
+
+class ChangeMembershipFromOrganization(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        m = kw.get('membership')
+        user = m.member
+
+        if m.status == 'active':
+            statusoptions = [
+                ew.Option(py_value='active',label='Active',selected=True),
+                ew.Option(py_value='closed',label='Closed',selected=False)]
+        elif m.status == 'closed':
+            statusoptions = [
+                ew.Option(py_value='closed',label='Closed',selected=True)]
+        elif m.status == 'invitation':
+            statusoptions = [
+                ew.Option(
+                    py_value='invitation',
+                    label='Pending invitation',
+                    selected=True),
+                ew.Option(
+                    py_value='remove',
+                    label='Remove invitation',
+                    selected=False)]
+        elif m.status == 'request':
+            statusoptions = [
+                ew.Option(
+                    py_value='request',label='Pending request',selected=True),
+                ew.Option(py_value='active',label='Accept',selected=False),
+                ew.Option(py_value='remove',label='Decline',selected=False)]
+ 
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="membershipid",
+                        attrs={'value':str(m._id)},
+                        show_errors=False),
+                    ew.HiddenField(
+                        name="requestfrom",
+                        attrs={'value':'organization'},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text='<a href="%s">%s</a>' % (
+                            user.url(), user.display_name),
+                        show_errors=False),
+                    ew.TextField(
+                        name='role',
+                        attrs=dict(value=m.role),
+                        show_errors=False),
+                    ew.SingleSelectField(
+                        name='status',
+                        show_errors=False,
+                        options = statusoptions),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Save'},
+                        show_errors=False)])]
+        return super(ChangeMembershipFromOrganization, self).display(**kw)
+
+class ChangeCollaborationStatusForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        coll = kw.get('collaboration')
+        proj = coll.project
+        projfield = '<a href="%s">%s</a>' % (proj.url(), proj.name)
+        
+        select_cooperation = (coll.collaborationtype=='cooperation')
+        if coll.status=='closed':
+            options=[ew.Option(py_value='closed',label='Closed',selected=True)]
+        elif coll.status=='active':
+            options=[
+                ew.Option(py_value='closed',label='Closed',selected=False),
+                ew.Option(py_value='active',label='Active',selected=True)]
+        elif coll.status=='invitation':
+            options=[
+                ew.Option(
+                    py_value='invitation',
+                    label='Pending invitation',
+                    selected=True),
+                ew.Option(
+                    py_value='active',
+                    label='Accept invitation',
+                    selected=False),
+                ew.Option(
+                    py_value='remove',
+                    label='Remove invitation',
+                    selected=False)]
+        elif coll.status=='request':
+            options=[
+                ew.Option(
+                    py_value='request',
+                    label='Pending request',
+                    selected=True),
+                ew.Option(
+                    py_value='remove',
+                    label='Remove request',
+                    selected=False)]
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="collaborationid",
+                        attrs={'value':str(coll._id)},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=projfield,
+                        show_errors=False),
+                    ew.SingleSelectField(
+                        name='collaborationtype',
+                        options = [
+                            ew.Option(
+                                py_value='cooperation', 
+                                selected=select_cooperation,
+                                label='Cooperation'),
+                            ew.Option(
+                                py_value='participation',
+                                selected=not select_cooperation,
+                                label='Participation')],
+                        validator=fev.UnicodeString(not_empty=True)),
+                    ew.SingleSelectField(
+                        name='status',
+                        options = options,
+                        validator=fev.UnicodeString(not_empty=True)),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Save'},
+                        show_errors=False)])]
+        return super(ChangeCollaborationStatusForm, self).display(**kw)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization_profile/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization_profile/__init__.py b/ForgeOrganization/forgeorganization/organization_profile/__init__.py
new file mode 100644
index 0000000..0eae989
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization_profile/__init__.py
@@ -0,0 +1 @@
+from .organization_main import OrganizationProfileApp

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization_profile/organization_main.py
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization_profile/organization_main.py b/ForgeOrganization/forgeorganization/organization_profile/organization_main.py
new file mode 100644
index 0000000..bb29f6d
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization_profile/organization_main.py
@@ -0,0 +1,293 @@
+import logging
+from pprint import pformat
+
+import pkg_resources
+from pylons import tmpl_context as c, app_globals as g
+from pylons import request
+from formencode import validators
+from tg import expose, redirect, validate, response, flash
+from webob import exc
+
+from allura import version
+from allura.app import DefaultAdminController
+from allura.app import Application, SitemapEntry
+from allura.lib import helpers as h
+from allura.lib.helpers import DateTimeConverter
+from allura.lib.security import require_access
+import allura.model as M
+from allura.model import User, Feed, ACE
+from allura.controllers import BaseController
+from allura.lib.decorators import require_post
+
+from forgeorganization.organization.model import Organization, WorkFields, Membership, ProjectInvolvement
+
+import forgeorganization.organization.widgets.forms as forms
+from allura.lib import validators as V
+from allura.lib.security import require_authenticated
+from allura.lib.decorators import require_post
+
+log = logging.getLogger(__name__)
+
+class Forms(object):
+    update_profile = forms.UpdateProfile()
+    add_work_field = forms.AddWorkField()
+    remove_work_field = forms.RemoveWorkField()
+    invite_user_form = forms.InviteUser()
+    admission_request_form = forms.RequestAdmissionForm()
+    request_collaboration_form = forms.RequestCollaborationForm()
+    def new_change_collaboration_status(self):
+        return forms.ChangeCollaborationStatusForm()
+    def new_change_membership_from_organization(self):
+        return forms.ChangeMembershipFromOrganization()
+
+F = Forms()
+
+class OrganizationProfileApp(Application):
+    __version__ = version.__version__
+    installable = False
+    tool_label = 'Profile'
+    permissions = ['configure', 'read', 'write',
+                    'unmoderated_post', 'post', 'moderate', 'admin']
+    config_options = Application.config_options
+    icons={
+        24:'images/home_24.png',
+        32:'images/home_32.png',
+        48:'images/home_48.png'
+    }
+
+    def __init__(self, user, config):
+        Application.__init__(self, user, config)
+        role_admin = M.ProjectRole.by_name('Admin')._id
+        role_anon = M.ProjectRole.by_name('*anonymous')._id
+        self.config.acl = [
+            M.ACE.allow(role_anon, 'read'),
+            M.ACE.allow(role_admin, 'admin')]
+        self.root = OrganizationProfileController()
+        self.admin = OrganizationProfileAdminController(self)
+
+    @property
+    @h.exceptionless([], log)
+    def sitemap(self):
+        return [SitemapEntry('Profile', '.')]
+
+    def admin_menu(self):
+        links = [SitemapEntry(
+            'Edit',
+            '%sadmin/organizationprofile' % c.project.organization_project_of.url())]
+        return links
+
+    def install(self, project):
+        pr = c.user.project_role()
+        if pr:
+            self.config.acl = [
+                ACE.allow(pr._id, perm)
+                for perm in self.permissions ]
+
+    def uninstall(self, project):
+        pass
+
+    def main_menu(self):
+        return [SitemapEntry('Profile', '.')]
+
+    def is_visible_to(self, user):
+        return True
+
+class OrganizationProfileController(BaseController):
+
+    @expose('jinja:forgeorganization:organization_profile/templates/organization_index.html')
+    def index(self, **kw):
+        organization = c.project.organization_project_of
+        if not organization:
+            raise exc.HTTPNotFound()
+        activecoll=[coll for coll in organization.project_involvements
+                    if coll.status=='active']
+        closedcoll=[p for p in organization.project_involvements 
+                    if p.status=='closed']
+        mlist=[m for m in organization.memberships if m.status=='active']
+        plist=[m for m in organization.memberships if m.status=='closed']
+        return dict(
+            forms = F,
+            ask_admission = (c.user not in [m.member for m in mlist]) and c.user != M.User.anonymous(),
+            workfields = WorkFields.query.find(),
+            organization=organization,
+            members = mlist,
+            past_members=plist,
+            active_collaborations=activecoll,
+            closed_collaborations=closedcoll)
+
+    @expose()
+    @require_post()
+    @validate(F.admission_request_form, error_handler=index)
+    def admission_request(self, role, **kw):
+        require_access(c.project, 'read')
+        m=Membership.insert(
+            role, 'request', c.project.organization_project_of._id, c.user._id)
+        flash('Request sent')
+        redirect(c.project.organization_project_of.url()+'organizationprofile')
+
+class OrganizationProfileAdminController(DefaultAdminController):
+    @expose('jinja:forgeorganization:organization_profile/templates/edit_profile.html')
+    def index(self, **kw):
+        require_access(c.project, 'admin')
+        
+        organization = c.project.organization_project_of
+        mlist=[m for m in organization.memberships if m.status!='closed']
+        clist=[el for el in organization.project_involvements 
+               if el.status!='closed']
+
+        return dict(
+            organization = organization,
+            members = mlist,
+            collaborations= clist,
+            forms = F)
+
+    @expose()
+    @require_post()
+    @validate(F.remove_work_field, error_handler=index)
+    def remove_work_field(self, **kw):
+        require_access(c.project, 'admin')
+        c.project.organization_project_of.removeWorkField(kw['workfield'])
+        flash('The organization profile has been successfully updated.')
+        redirect(c.project.organization_project_of.url()+'admin/organizationprofile')
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def add_work_field(self, workfield, **kw):
+        require_access(c.project, 'admin')
+        workfield = WorkFields.getById(workfield)
+
+        if workfield is None:
+            flash("Invalid workfield. Select a valid value.", "error")
+            redirect(c.project.organization_project_of.url()+'admin/organizationprofile')
+        c.project.organization_project_of.addWorkField(workfield)
+        flash('The organization profile has been successfully updated.')
+        redirect(c.project.organization_project_of.url()+'admin/organizationprofile')
+
+    @expose()
+    @require_post()
+    @validate(F.invite_user_form, error_handler=index)
+    def invite_user(self, **kw):
+        require_access(c.project, 'admin')
+        username = kw['username']
+        user = M.User.query.get(username=kw['username'])
+        if not user:
+            flash(
+                '''The username "%s" doesn't belong to any user on the forge'''\
+                % username, "error")
+            redirect(c.project.organization_project_of.url() + 'admin/organizationprofile')
+
+        invitation = Membership.insert(kw['role'], 'invitation', 
+            c.project.organization_project_of._id, user._id)
+        if invitation:
+            flash(
+                'The user '+ username +' has been successfully invited to '+ \
+                'become a member of the organization.')
+        else:
+            flash(
+                username+' is already a member of the organization.', 'error')
+
+        redirect(c.project.organization_project_of.url()+'admin/organizationprofile')
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def change_membership(self, **kw):        
+        membershipid = kw['membershipid']
+        memb = Membership.getById(membershipid)
+        status = kw['status']
+
+        return_url = memb.organization.url() + 'admin/organizationprofile'
+
+        if status == 'remove':
+            old_status = memb.status
+            if memb.status in ['invitation', 'request']:
+                Membership.delete(memb)
+                flash('The pending %s has been removed.' % old_status)
+                redirect(return_url)
+                return
+            else:
+                flash(
+                    "You don't have the permission to perform this action.", 
+                    "error")
+                redirect(return_url)
+                return
+
+        allowed=True
+        if memb.status=='closed' and status!='closed':
+            allowed=False
+        if memb.status=='invitation' and status=='active':
+            allowed=False
+
+        if allowed:
+            memb.setStatus(status)
+            memb.role = kw.get('role')
+            flash('The membership has been successfully updated.')
+        else:
+            flash("You are not allowed to perform this action.")
+        redirect(return_url)
+
+    @expose()
+    @require_post()
+    @validate(F.request_collaboration_form, error_handler=index)
+    def send_collaboration_request(self, project_url_name, collaboration_type, **kw):
+        require_access(c.project, 'admin')
+        project=M.Project.query.get(shortname=project_url_name)
+        if not project:
+            flash(
+                "Invalid URL name. Please, insert the URL name of an existing "+\
+                "project.", "error")
+        else:
+            ProjectInvolvement.insert('request', collaboration_type, 
+                c.project.organization_project_of._id, project._id)
+            flash("Collaboration request successfully sent.")
+        redirect('%sadmin/organizationprofile' % c.project.organization_project_of.url())
+            
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def update_collaboration_status(self, collaborationid, collaborationtype, status, **kw):
+        require_access(c.project, 'admin')
+
+        coll = ProjectInvolvement.getById(collaborationid)
+
+        allowed = True
+        if coll.status != status:
+            if coll.status=='invitation' and status not in ['active','remove']:
+                allowed=False
+            elif coll.status=='closed':
+                allowed=False
+            elif coll.status=='active' and status!='closed':
+                allowed=False
+            elif coll.status=='request' and status !='remove':
+                allowed=False
+
+        if allowed:
+            if status=='closed':
+                collaborationtype=coll.collaborationtype
+
+            if status=='remove':
+                ProjectInvolvement.delete(coll._id)
+            else:
+                coll.collaborationtype=collaborationtype
+                coll.setStatus(status)
+            flash('The information about this collaboration has been updated')
+        else:
+            flash("You are not allowed to perform this action", "error")
+        redirect('%sadmin/organizationprofile' % coll.organization.url())
+
+    @expose()
+    @require_post()
+    @validate(F.update_profile, error_handler=index)
+    def change_data(self, **kw):
+        require_access(c.project, 'admin')
+
+        c.project.organization_project_of.organization_type = kw['organization_type']
+        c.project.organization_project_of.fullname = kw['fullname']
+        c.project.organization_project_of.description = kw['description']
+        c.project.organization_project_of.headquarters = kw['headquarters']
+        c.project.organization_project_of.dimension = kw['dimension']
+        c.project.organization_project_of.website = kw['website']
+
+        flash('The organization profile has been successfully updated.')
+        redirect(c.project.organization_project_of.url() + 'admin/organizationprofile')

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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization_profile/templates/edit_profile.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization_profile/templates/edit_profile.html b/ForgeOrganization/forgeorganization/organization_profile/templates/edit_profile.html
new file mode 100644
index 0000000..aca2a57
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization_profile/templates/edit_profile.html
@@ -0,0 +1,111 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Change organization data{% endblock %}
+
+{% block header %}Change organization data{% endblock %}
+
+{% block content %}
+
+  <ul><li><a href="{{organization.url()}}organizationprofile">Click here</a> to check your public profile.</ul>
+  <div class="grid-20">
+    <h2>Data to be included in {{organization.fullname}}'s profile</h2>
+    {{forms.update_profile.display(organization=organization, action=organization.url()+'admin/organizationprofile/change_data') }}
+  </div>
+
+  <div class="grid-20" style="clear:both;">
+    <h2>Work Fields</h2>
+    {% if organization.workfields %}
+      <table>
+        <tr>
+          <thead>
+            <th>Work field</th>
+            <th>Description</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for wf in organization.getWorkfields() %}
+          {{forms.remove_work_field.display(workfield=wf, action=organization.url()+'admin/organizationprofile/remove_work_field')}} 
+        {%endfor%}
+      </table>
+    {% else %}
+      <p>At the moment, there are no working fields set for this organization.</p>
+    {% endif %}
+    <h3>Add a new work field</h3>
+    <div class="grid-20" style="margin:0;">
+      {{forms.add_work_field.display(organization=organization, action=organization.url()+'admin/organizationprofile/add_work_field') }}
+    </div>
+  </div>
+
+  <div class="grid-20">
+    <h2>Members</h2>
+
+    {% if members %}
+      <table>
+        <thead>
+          <tr>
+            <th>Name</th>
+            <th>Role</th>
+            <th>Status</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for membership in members %}
+            <tr>
+              {{forms.new_change_membership_from_organization().display(
+                  membership=membership,
+                  action=membership.organization.url()+'admin/organizationprofile/change_membership')}}
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    {% else %}
+      <p>This organization doesn't have any enrolled member.</p>
+    {% endif %}
+    <h3>Add a new user</h3>
+    <p>
+      You can add a member of your organization to the above list by filling the following form with his or her 
+      username and the user's role within the organization.
+    </p>
+    {{forms.invite_user_form.display(action=organization.url()+'admin/organizationprofile/invite_user')}}
+
+  </div>
+
+  <div class="grid-20">
+    <h2>Collaborations</h2>
+
+    {% if collaborations %}
+      <h3>Edit existing collaborations</h3>
+      <table>
+        <thead>
+          <tr>
+            <th>Project</th>
+            <th>Collaboration type</th>
+            <th>Status</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for org in collaborations %}
+            <tr>
+              {{forms.new_change_collaboration_status().display(
+                    collaboration=org,
+                    action=organization.url()+'admin/organizationprofile/update_collaboration_status')}}
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    {% else %}
+      <p>At the moment, this organization doesn't collaborate in any project.</p>
+    {% endif %}
+
+    <h3>Add a new collaboration</h3>
+    <p>
+      If you want to include a new collaboration in your profile, you can look for the project and send an admission request.
+      Otherwise, you can add it using the following form.
+    </p>
+    {{forms.request_collaboration_form.display(action=organization.url()+'admin/organizationprofile/send_collaboration_request')}}
+  </div>
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/787ef235/ForgeOrganization/forgeorganization/organization_profile/templates/organization_index.html
----------------------------------------------------------------------
diff --git a/ForgeOrganization/forgeorganization/organization_profile/templates/organization_index.html b/ForgeOrganization/forgeorganization/organization_profile/templates/organization_index.html
new file mode 100644
index 0000000..3c04a96
--- /dev/null
+++ b/ForgeOrganization/forgeorganization/organization_profile/templates/organization_index.html
@@ -0,0 +1,206 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{organization.fullname}} / Profile{% endblock %}
+
+{% block header %}{{ organization.fullname }}'s profile{% endblock %}
+
+{% block extra_css %}
+  <link rel="stylesheet" type="text/css"
+        href="{{g.app_static('css/user_profile.css')}}"/>
+{% endblock %}
+
+{% block head %}
+  <link rel="alternate" type="application/rss+xml" title="RSS" href="feed.rss">
+  <link rel="alternate" type="application/atom+xml" title="Atom" href="feed.atom">
+{% endblock %}
+
+{% block actions %}
+  <a href="{{c.app.url}}feed.rss" title="Follow"><b data-icon="{{g.icons['feed'].char}}" class="ico {{g.icons['feed'].css}}"></b></a>
+{% endblock %}
+
+{% block content %}
+
+  {% if not organization %}
+
+    <div class="grid-20">
+      This organization doesn't exists.
+    </div>
+
+  {% else %}
+
+    <h2>{{organization.fullname}} – General data</h2>
+    <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+      <label class="grid-4">Organization Type:</label>
+      <label class="grid-14">{{organization.organization_type}}</label>
+    </div>
+
+    {% if organization.description %}
+      <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+        <label class="grid-4">Description:</label>
+        <label class="grid-14">{{organization.description}}</label>
+      </div>
+    {% endif %}
+
+    {% if organization.dimension and organization.dimension != 'Unknown' %}
+      <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+        <label class="grid-4">Dimension:</label>
+        <label class="grid-14">
+          {% if organization.dimension == 'Small' %}Small – No more than 50 members{% endif %}
+          {% if organization.dimension == 'Medium' %}Medium – Between 51 and 250 members{% endif %}
+          {% if organization.dimension == 'Large' %}Big – More than 250 members{% endif %}
+        </label>
+      </div>
+    {% endif %}
+
+    {% if organization.headquarters %}
+      <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+        <label class="grid-4">Headquarters:</label>
+        <label class="grid-14">{{organization.headquarters}}</label>
+      </div>
+    {% endif %}
+
+    {% if organization.website %}
+      <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+        <label class="grid-4">Website:</label>
+        <label class="grid-14"><a href="{{organization.website}}">{{organization.website}}</a></label>
+      </div>
+    {% endif %}
+
+    {% if organization.getWorkfields() %}
+      <div class="grid-20" style="margin-top:5px;margin-left:5px;">
+        <label class="grid-4">Workfields:</label>
+        <div class="grid-14">
+          <ul>
+            {% for wf in organization.getWorkfields() %}
+              <li>{{wf.name}} – {{wf.description}}</li>
+            {% endfor %}
+          </ul>
+        </div>
+      </div>
+    {% endif %}
+
+    <h2 style="clear:both;">Members</h2>
+    <div class="grid-20">
+      {% if members %}
+        <h3>Currently enrolled members</h3>
+        <table>
+          <thead>
+            <tr>
+              <th>Name</th>
+              <th>Role</th>
+              <th>Admission date on the forge</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for member in members -%}
+              <tr>
+                <td><a href="{{member.member.url()}}">{{member.member.display_name}}</a></td>
+                <td>{{member.role}}</td>
+                <td>{{member.startdate.strftime("%d %B %Y")}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% else %}
+        <p>Currently, this organization doesn't have any member.</p>
+      {% endif %}
+    </div>
+
+    <div class="grid-20">
+      {% if past_members %}
+        <h3>Past members of the organization</h3>
+        <table>
+          <thead>
+            <tr>
+              <th>Name</th>
+              <th>Role</th>
+              <th>Admission date on the forge</th>
+              <th>Closing membership date</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for member in past_members -%}
+              <tr>
+                <td><a href="{{member.member.url()}}">{{member.member.display_name}}</a></td>
+                <td>{{member.role}}</td>
+                <td>{{member.startdate.strftime("%d %B %Y")}}</td>
+                <td>{{member.closeddate.strftime("%d %B %Y")}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% endif %}
+    </div>
+  
+    {% if ask_admission %}
+      <div class="grid-20" style="clear:both;">
+        <h3>Are you a member of this organization?</h3>
+        <p>
+          If you are a member of this organization, you can send a request to appear in the list above. Before being admitted
+          to the organization, an administrator of the organization profile has to confirm your enrollment.
+        </p>
+        <div class="grid-20" style="margin:0;clear:both;">
+          {{forms.admission_request_form.display(action=organization.url()+'organizationprofile/admission_request')}}
+        </div>
+      </div>
+    {% endif %}
+
+    <h2 style="clear:both;">Projects and collaborations</h2>
+    {%if active_collaborations %}
+      <div class="grid-20">
+        <h3>Active collaborations</h3>
+      </div>
+      <div class="grid-20">
+        <table>
+          <thead>
+            <th>Project</th>
+            <th>Collaboration type</th>
+            <th>Start date</th>
+          </thead>
+          <tbody>
+            {% for collaboration in active_collaborations %}
+              <tr>
+                <td><a href="{{collaboration.project.url()}}">{{collaboration.project.name}}</a></td>
+                <td>{{collaboration.collaborationtype.capitalize()}}</td>
+                <td>{{collaboration.startdate.strftime("%d %B %Y")}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    {% endif %}
+
+    {%if closed_collaborations %}
+      <div class="grid-20">
+        <h3>Past projects collaborations</h3>
+      </div>
+      <div class="grid-20">
+        <table>
+          <thead>
+            <th>Project</th>
+            <th>Collaboration type</th>
+            <th>Start date</th>
+            <th>End date</th>
+          </thead>
+          <tbody>
+            {% for collaboration in closed_collaborations %}
+              <tr>
+                <td><a href="{{collaboration.project.url()}}">{{collaboration.project.name}}</a></td>
+                <td>{{collaboration.collaborationtype.capitalize()}}</td>
+                <td>{{collaboration.startdate.strftime("%d %B %Y")}}</td>
+                <td>{{collaboration.closeddate.strftime("%d %B %Y")}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    {% endif %}
+
+    {% if not (closed_collaborations or active_collaborations) %}
+      <div class="grid-18"><p>This organization has never collaborated to any project.</p></div>
+    {% endif %}
+
+  {% endif %}
+  
+{% endblock %}

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


[5/5] git commit: [#5566] include organizationstats tests in run_tests

Posted by st...@apache.org.
[#5566] include organizationstats tests in run_tests


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

Branch: refs/heads/si/5566
Commit: aa1a90dbf3768f5042321ad1ef2b2784c7035dde
Parents: 787ef23
Author: Stefano Invernizzi <st...@apache.org>
Authored: Thu Apr 4 00:21:18 2013 +0200
Committer: Stefano Invernizzi <st...@apache.org>
Committed: Thu Apr 4 00:21:18 2013 +0200

----------------------------------------------------------------------
 run_tests |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/aa1a90db/run_tests
----------------------------------------------------------------------
diff --git a/run_tests b/run_tests
index 80a3079..e4e6945 100755
--- a/run_tests
+++ b/run_tests
@@ -23,6 +23,7 @@ if [ "$TEST_MODULES"  == "" ]; then
     ForgeActivity \
     ForgeShortUrl \
     ForgeUserStats \
+    ForgeOrganizationStats \
     "
 fi