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:26 UTC
[4/5] git commit: organization and organization stats
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