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