You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by jo...@apache.org on 2012/12/05 18:27:48 UTC

[23/34] [#5289] Added features to include personal details

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 0c2c060..0ef414a 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -9,6 +9,7 @@ from webob import exc as wexc
 
 import allura.tasks.repo_tasks
 from allura import model as M
+from allura.model.project import TroveCategory
 from allura.lib import validators as V
 from allura.lib.oid_helper import verify_oid, process_oid
 from allura.lib.security import require_authenticated, has_access
@@ -43,6 +44,19 @@ class F(object):
     registration_form = forms.RegistrationForm(action='/auth/save_new')
     oauth_application_form = OAuthApplicationForm(action='register')
     oauth_revocation_form = OAuthRevocationForm(action='revoke_oauth')
+    change_personal_data_form = forms.PersonalDataForm()
+    add_socialnetwork_form = forms.AddSocialNetworkForm()
+    remove_socialnetwork_form = forms.RemoveSocialNetworkForm()
+    add_telnumber_form = forms.AddTelNumberForm()
+    add_website_form = forms.AddWebsiteForm()
+    skype_account_form = forms.SkypeAccountForm()
+    remove_textvalue_form = forms.RemoveTextValueForm()
+    add_timeslot_form = forms.AddTimeSlotForm()
+    remove_timeslot_form = forms.RemoveTimeSlotForm()
+    add_inactive_period_form = forms.AddInactivePeriodForm()
+    remove_inactive_period_form = forms.RemoveInactivePeriodForm()
+    save_skill_form = forms.AddUserSkillForm()
+    remove_skill_form = forms.RemoveSkillForm()
 
 class AuthController(BaseController):
 
@@ -278,8 +292,83 @@ class AuthController(BaseController):
                     allow_write=has_access(c.app, 'write')(user=user),
                     allow_create=has_access(c.app, 'create')(user=user))
 
+class UserSkillsController(BaseController):
+
+    def __init__(self, category=None):
+        self.category = category
+        super(UserSkillsController, self).__init__()
+
+    @expose()
+    def _lookup(self, catshortname, *remainder):
+        cat = M.TroveCategory.query.get(shortname=catshortname)
+        return UserSkillsController(category=cat), remainder
+
+    @expose('jinja:allura:templates/user_skills.html')
+    def index(self, **kw):
+        require_authenticated()
+
+        l = []
+        parents = []
+        if kw.get('selected_category') is not None:
+            selected_skill = M.TroveCategory.query.get(trove_cat_id=int(kw.get('selected_category')))
+        elif self.category:
+            selected_skill = self.category
+        else:
+            l = M.TroveCategory.query.find(dict(trove_parent_id=0, show_as_skill=True))
+            selected_skill = None
+        if selected_skill:
+            l = [scat for scat in selected_skill.subcategories
+                 if scat.show_as_skill]
+            temp_cat = selected_skill.parent_category
+            while temp_cat:
+                parents = [temp_cat] + parents
+                temp_cat = temp_cat.parent_category
+        return dict(
+            skills_list = l,
+            selected_skill = selected_skill,
+            parents = parents, 
+            add_details_fields=(len(l)==0))
+
+    @expose()
+    @require_post()
+    @validate(F.save_skill_form, error_handler=index)
+    def save_skill(self, **kw):
+        require_authenticated()
+        
+        trove_id = int(kw.get('selected_skill'))
+        category = M.TroveCategory.query.get(trove_cat_id=trove_id)
+
+        new_skill = dict(
+            category_id=category._id,
+            level=kw.get('level'),
+            comment=kw.get('comment'))
+
+        s = [skill for skill in c.user.skills 
+             if str(skill.category_id) != str(new_skill['category_id'])]
+        s.append(new_skill)
+        c.user.set_pref('skills', s)
+        flash('Your skills list was successfully updated!')
+        redirect('/auth/prefs/user_skills')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_skill_form, error_handler=index)
+    def remove_skill(self, **kw):
+        require_authenticated()
+
+        trove_id = int(kw.get('categoryid'))
+        category = M.TroveCategory.query.get(trove_cat_id=trove_id)
+
+        s = [skill for skill in c.user.skills 
+             if str(skill.category_id) != str(category._id)]
+        c.user.set_pref('skills', s)
+        flash('Your skills list was successfully updated!')
+        redirect('/auth/prefs/user_skills')
+
 class PreferencesController(BaseController):
 
+    user_skills = UserSkillsController()
+
     @with_trailing_slash
     @expose('jinja:allura:templates/user_preferences.html')
     def index(self, **kw):
@@ -457,6 +546,119 @@ class PreferencesController(BaseController):
 
     @expose()
     @require_post()
+    @validate(F.change_personal_data_form, error_handler=index)
+    def change_personal_data(self, **kw):
+        require_authenticated()
+        c.user.set_pref('sex', kw['sex'])
+        c.user.set_pref('birthdate', kw.get('birthdate'))
+        localization={'country':kw.get('country'), 'city':kw.get('city')}
+        c.user.set_pref('localization', localization)
+        c.user.set_pref('timezone', kw['timezone'])
+
+        flash('Your personal data was successfully updated!')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(F.add_socialnetwork_form, error_handler=index)
+    def add_social_network(self, **kw):
+        require_authenticated()
+        c.user.add_socialnetwork(kw['socialnetwork'], kw['accounturl'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_socialnetwork_form, error_handler=index)
+    def remove_social_network(self, **kw):
+        require_authenticated()
+        c.user.remove_socialnetwork(kw['socialnetwork'], kw['account'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.add_telnumber_form, error_handler=index)
+    def add_telnumber(self, **kw):
+        require_authenticated()
+        c.user.add_telephonenumber(kw['newnumber'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_textvalue_form, error_handler=index)
+    def remove_telnumber(self, **kw):
+        require_authenticated()
+        c.user.remove_telephonenumber(kw['oldvalue'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.add_website_form, error_handler=index)
+    def add_webpage(self, **kw):
+        require_authenticated()
+        c.user.add_webpage(kw['newwebsite'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_textvalue_form, error_handler=index)
+    def remove_webpage(self, **kw):
+        require_authenticated()
+        c.user.remove_webpage(kw['oldvalue'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.skype_account_form, error_handler=index)
+    def skype_account(self, **kw):
+        require_authenticated()
+        c.user.set_pref('skypeaccount', kw['skypeaccount'])
+        flash('Your personal contacts were successfully updated!')
+        redirect('.#Contacts')
+
+    @expose()
+    @require_post()
+    @validate(F.add_timeslot_form, error_handler=index)
+    def add_timeslot(self, **kw):
+        require_authenticated()
+        c.user.add_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.#Availability')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_timeslot_form, error_handler=index)
+    def remove_timeslot(self, **kw):
+        require_authenticated()
+        c.user.remove_timeslot(kw['weekday'], kw['starttime'], kw['endtime'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.#Availability')
+
+    @expose()
+    @require_post()
+    @validate(F.add_inactive_period_form, error_handler=index)
+    def add_inactive_period(self, **kw):
+        require_authenticated()
+        c.user.add_inactive_period(kw['startdate'], kw['enddate'])
+        flash('Your inactivity periods were successfully updated!')
+        redirect('.#Availability')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_inactive_period_form, error_handler=index)
+    def remove_inactive_period(self, **kw):
+        require_authenticated()
+        c.user.remove_inactive_period(kw['startdate'], kw['enddate'])
+        flash('Your availability timeslots were successfully updated!')
+        redirect('.#Availability')
+
+    @expose()
+    @require_post()
     def upload_sshkey(self, key=None):
         ap = plugin.AuthenticationProvider.get(request)
         try:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/controllers/root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/root.py b/Allura/allura/controllers/root.py
index 4fdf305..19719e1 100644
--- a/Allura/allura/controllers/root.py
+++ b/Allura/allura/controllers/root.py
@@ -22,6 +22,7 @@ from allura.controllers.error import ErrorController
 from allura import model as M
 from allura.lib.widgets import project_list as plw
 from .auth import AuthController
+from .trovecategories import TroveCategoryController
 from .search import SearchController, ProjectBrowseController
 from .static import NewForgeController
 from .site_admin import SiteAdminController
@@ -58,6 +59,8 @@ class RootController(WsgiDispatchController):
     nf.admin = SiteAdminController()
     search = SearchController()
     rest = RestController()
+    if config.get('trovecategories.enableediting', 'false')=='true':
+        categories=TroveCategoryController()
 
     def __init__(self):
         n_url_prefix = '/%s/' % request.path.split('/')[1]

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/controllers/trovecategories.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/trovecategories.py b/Allura/allura/controllers/trovecategories.py
new file mode 100644
index 0000000..773e506
--- /dev/null
+++ b/Allura/allura/controllers/trovecategories.py
@@ -0,0 +1,191 @@
+import logging, string, os
+from urllib import urlencode
+
+import bson
+from tg import expose, session, flash, redirect, validate, config
+from tg.decorators import with_trailing_slash
+from pylons import c, g, request, response
+from string import digits, lowercase
+
+from allura.lib.security import require_authenticated
+from allura import model as M
+from allura.lib.decorators import require_post
+from allura.controllers import BaseController
+from allura.lib.widgets import forms
+from allura.model import TroveCategory
+
+class F(object):
+    remove_category_form = forms.RemoveTroveCategoryForm()
+    add_category_form = forms.AddTroveCategoryForm()
+
+class TroveCategoryController(BaseController):
+    @expose()
+    def _lookup(self, catshortname, *remainder):
+        cat = M.TroveCategory.query.get(shortname=catshortname)
+        return TroveCategoryController(category=cat), remainder
+        
+    def __init__(self, category=None):
+        self.category = category
+        super(TroveCategoryController, self).__init__()
+
+    @expose('jinja:allura:templates/trovecategories.html')
+    def index(self, **kw):
+        require_authenticated()
+
+        if self.category:
+            selected_cat = self.category
+            l = self.category.subcategories
+            hierarchy = []
+            temp_cat = self.category.parent_category
+            while temp_cat:
+               hierarchy = [temp_cat] + hierarchy
+               temp_cat = temp_cat.parent_category
+        else:
+            l = M.TroveCategory.query.find(dict(trove_parent_id=0))
+            selected_cat = None
+            hierarchy = []
+        return dict(
+            categories=l, 
+            selected_cat=selected_cat, 
+            hierarchy=hierarchy)
+
+    @expose()
+    @require_post()
+    @validate(F.add_category_form, error_handler=index)
+    def create(self, **kw):
+        require_authenticated()
+
+        name = kw.get('categoryname')
+        upper_id = int(kw.get('uppercategory_id', 0))
+
+        upper = M.TroveCategory.query.get(trove_cat_id=upper_id)
+        if upper_id == 0:
+            path = name
+            show_as_skill = True
+        elif upper is None:
+            flash('Invalid upper category.', "error")
+            redirect('/categories')
+            return
+        else:
+            path = upper.fullpath + " :: " + name
+            show_as_skill = upper.show_as_skill
+
+        newid=max([el.trove_cat_id for el in M.TroveCategory.query.find()]) + 1
+        shortname=name.replace(" ", "_").lower()
+        shortname=''.join([(c if (c in digits or c in lowercase) else "_")
+                           for c in shortname])
+
+        oldcat=M.TroveCategory.query.get(shortname=shortname)
+        if oldcat:
+            flash('Category "%s" already exists.' % name, "error")
+        else:
+            category = M.TroveCategory(
+               trove_cat_id=newid,
+               trove_parent_id=upper_id,
+               fullname=name,
+               shortname=shortname,
+               fullpath=path,
+               show_as_skill=show_as_skill)
+            if category:
+                flash('Category "%s" successfully created.' % name)
+            else:
+                flash('An error occured while crearing the category.', "error")
+        if upper:
+            redirect('/categories/%s' % upper.shortname)
+        else:
+            redirect('/categories')
+
+    @expose()
+    @require_post()
+    @validate(F.remove_category_form, error_handler=index)
+    def remove(self, **kw):
+        require_authenticated()
+
+        cat = M.TroveCategory.query.get(trove_cat_id=int(kw['categoryid']))
+        if cat.trove_parent_id:
+            parent=M.TroveCategory.query.get(trove_cat_id=cat.trove_parent_id)
+            redirecturl = '/categories/%s' % parent.shortname
+        else:
+            redirecturl = '/categories'
+        if len(cat.subcategories) > 0:
+            m = "This category contains at least one sub-category, "
+            m = m + "therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl)
+            return
+        
+        if len(M.User.withskill(cat)) > 0:
+            m = "This category is used as a skill by at least a user, "
+            m = m + "therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_root_database=cat._id):
+            m = "This category is used as a database by at least a project, "
+            m = m + "therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_developmentstatus=cat._id):
+            m = "This category is used as development status by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_audience=cat._id):
+            m = "This category is used as intended audience by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_license=cat._id):
+            m = "This category is used as a license by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_os=cat._id):
+            m = "This category is used as operating system by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_language=cat._id):
+            m = "This category is used as programming language by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_topic=cat._id):
+            m = "This category is used as a topic by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_natlanguage=cat._id):
+            m = "This category is used as a natural language by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        if M.Project.query.get(trove_environment=cat._id):
+            m = "This category is used as an environment by at least a "
+            m = m + "project, therefore it can't be removed."
+            flash(m, "error")
+            redirect(redirecturl) 
+            return
+
+        M.TroveCategory.delete(cat)
+
+        flash('Category removed.')
+        redirect(redirecturl)
+

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/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 1f6819d..2614953 100644
--- a/Allura/allura/ext/user_profile/templates/user_index.html
+++ b/Allura/allura/ext/user_profile/templates/user_index.html
@@ -35,6 +35,207 @@
       {% endfor %}
     </ul>
   </div>
+
+  <div class="grid-24">
+    <div class="grid-24" style="margin:0;"><b>Personal data</b></div>
+    {% if user.get_pref('sex') == 'Male' or user.get_pref('sex') == 'Female' %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Gender:</div>
+        <div class="grid-8">{{user.get_pref('sex')}}</div>
+      </div>
+    {% endif %}
+    {% if user.get_pref('birthdate') %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Birthdate:</div>
+        <div class="grid-8">
+          {{ user.get_pref('birthdate').strftime('%d %B %Y')}}
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('localization').country or user.get_pref('localization').city %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Localization:</div>
+        <div class="grid-8">
+          {% if user.get_pref('localization').city %}
+            {{user.get_pref('localization').city}}{{ ',' if user.get_pref('localization').country else '' }}
+          {% endif %}
+          {% if user.get_pref('localization').country %}
+            {{user.get_pref('localization').country}}
+         {% endif %}
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('timezone') %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Timezone:</div>
+        <div class="grid-8">
+          {{user.get_pref('timezone')}}
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('socialnetworks')|length > 0 %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Social networks:</div>
+        <div class="grid-18">
+           {{user.get_pref('display_name')}}'s account(s):
+           <ul>
+             {% for i in user.get_pref('socialnetworks') %}            
+                <li>{{i.socialnetwork}}: <a href="{{i.accounturl}}">{{i.accounturl}}</a></li>
+             {% endfor %}
+           </ul>
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('webpages')|length > 0 %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Websites:</div>
+        <div class="grid-18">
+           {{user.get_pref('display_name')}}'s website(s):
+           <ul>
+             {% for i in user.get_pref('webpages') %}            
+                <li><a href="{{i}}">{{i}}</a></li>
+             {% endfor %}
+           </ul>
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('telnumbers')|length > 0 %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Telephone number(s):</div>
+        <div class="grid-18">
+           {{user.get_pref('display_name')}}'s telephone number(s):
+           <ul>
+             {% for i in user.get_pref('telnumbers') %}            
+                <li>{{i}}</li>
+             {% endfor %}
+           </ul>
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('skypeaccount') %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Skype account:</div>
+        <div class="grid-8">{{user.get_pref('skypeaccount')}}</div>
+      </div>
+    {% endif %}
+
+    {% if user.get_pref('timezone') and user.get_availability_timeslots() |length > 0 %}
+      <div class="grid-24" style="margin:0;">
+        <div class="grid-4">Availability:</div>
+
+          {% if c.user.get_pref('timezone') %}
+          <div class="grid-18" id="timeslotsconverted" style="visibility: visible; display:none;">
+             {{user.get_pref('display_name')}}'s availability time-slots. 
+             <div style="float:right;">
+               See timeslots in:
+               <a href="JavaScript:void(0);" onclick="changeTimezone('utc')">UTC</a> | 
+               <a href="JavaScript:void(0);" onclick="changeTimezone('local')"> 
+                  {{user.get_pref('display_name')}}'s local time
+               </a> |
+               <b>Your local time</b> 
+             </div>
+             <ul>
+               {% for i in user.get_localized_availability(c.user.get_pref('timezone')) %}
+                  <li>{{i.week_day}}: from {{i.start_time.strftime("%H:%M")}} to {{i.end_time.strftime("%H:%M")}} </li>
+               {% endfor %}
+             </ul>
+          </div>
+          {% endif %}
+
+          <div class="grid-18" id="timeslotsutc" style="visibility: visible; display:block;">
+             {{user.get_pref('display_name')}}'s availability time-slots. 
+             <div style="float:right;">
+               See timeslots in:
+               <b>UTC</b> | 
+               <a href="JavaScript:void(0);" onclick="changeTimezone('local')"> 
+                  {{user.get_pref('display_name')}}'s local time
+               </a> 
+               {% if c.user.get_pref('timezone') %} |
+                  <a href="JavaScript:void(0);" onclick="changeTimezone('converted')">
+                    Your local time
+                  </a>
+               {% endif %}
+             </div>
+             <ul>
+               {% for i in user.get_localized_availability('utc') %}
+                  <li>{{i.week_day}}: from {{i.start_time.strftime("%H:%M")}} to {{i.end_time.strftime("%H:%M")}} </li>
+               {% endfor %}
+             </ul>
+          </div>
+
+          <div class="grid-18" id="timeslotslocal" style="visibility: visible; display:none;">
+             {{user.get_pref('display_name')}}'s availability time-slots. 
+             <div style="float:right;">
+               See timeslots in:
+               <a href="JavaScript:void(0);" onclick="changeTimezone('utc')">UTC</a> | 
+               <b> 
+                  {{user.get_pref('display_name')}}'s local time
+               </b> 
+               {% if c.user.get_pref('timezone') %} |
+                  <a href="JavaScript:void(0);" onclick="changeTimezone('converted')">
+                    Your local time
+                  </a>
+               {% endif %}
+             </div>
+             <ul>
+               {% for i in user.get_availability_timeslots() %}
+                  <li>{{i.week_day}}: from {{i.start_time.strftime("%H:%M")}} to {{i.end_time.strftime("%H:%M")}} </li>
+               {% endfor %}
+             </ul>
+          </div>
+
+        </div>
+      </div>
+    {% endif %}
+
+    {% if user.get_inactive_periods(include_past_periods=False)|length > 0 %}
+      <div class="grid-24">
+        <div class="grid-4">Inactive periods:</div>
+        <div class="grid-18">
+          This user won't be able to work on the forge in the following period(s):
+          <ul>
+            {% for p in user.get_inactive_periods(include_past_periods=False) %}
+              <li>From {{p.start_date.strftime('%d %B %Y')}} to {{p.end_date.strftime('%d %B %Y')}}.</li>
+            {% endfor %}
+        </div>
+      </div>
+    {% endif %}
+
+  </div><!-- end of Personal data section -->
+  <div class="grid-24">
+    <b>Current {{user.get_pref('display_name')}}'s skills list</b>
+    <div class="grid-24">
+      {% if user.get_skills()|length > 0 %}
+        <table>
+          <thead>
+            <tr>
+              <th>Skill</th>
+              <th>Level</th>
+              <th>Comments</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for s in user.get_skills() %}
+              <tr>
+                <td>{{s.skill.fullpath}}</td>
+                <td>{{s.level}}</td>
+                <td>{{s.comment}}</td>
+              </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      {% else %}
+        <div class="grid-24">At the moment, {{user.get_pref('display_name')}}'s skills list is empty!</div>
+      {% endif %}
+    </div>
+  </div>
+
   {% if c.user.username == user.username %}
       <div class="address-list grid-18">
         <b>Email Addresses</b>
@@ -63,3 +264,14 @@
       </div>
   {% endif %}
 {% endblock %}
+
+{% block extra_js %}
+  <script type="text/javascript">
+     function changeTimezone(opt){
+       $("#timeslotslocal").hide();
+       $("#timeslotsutc").hide();
+       $("#timeslotsconverted").hide();
+       $("#timeslots" + opt).show();
+     }
+  </script>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 93f4e3a..0c2aca4 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -625,6 +625,150 @@ class ThemeProvider(object):
         from allura.lib.widgets.forms import PasswordChangeForm
         return PasswordChangeForm(action='/auth/prefs/change_password')
 
+    @LazyProperty 
+    def personal_data_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page 
+        ''' 
+        from allura.lib.widgets.forms import PersonalDataForm 
+        return PersonalDataForm()
+
+    @LazyProperty 
+    def add_telnumber_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page to 
+                 allow adding a telephone number.
+        ''' 
+        from allura.lib.widgets.forms import AddTelNumberForm 
+        return AddTelNumberForm()
+
+    @LazyProperty 
+    def add_website_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page to 
+                 allow adding a personal website url.
+        ''' 
+        from allura.lib.widgets.forms import AddWebsiteForm 
+        return AddWebsiteForm()
+
+    @LazyProperty 
+    def skype_account_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page to 
+                 allow setting the user's Skype account.
+        ''' 
+        from allura.lib.widgets.forms import SkypeAccountForm 
+        return SkypeAccountForm()
+
+    @LazyProperty 
+    def remove_textvalue_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page to 
+                 allow removing a single text value from a list.
+        ''' 
+        from allura.lib.widgets.forms import RemoveTextValueForm 
+        return RemoveTextValueForm()
+
+    @LazyProperty 
+    def add_socialnetwork_form(self):
+        '''
+        :return: None, or an easywidgets Form to render on  the user preferences page to 
+                 allow adding a social network account.
+        '''
+        from allura.lib.widgets.forms import AddSocialNetworkForm
+        return AddSocialNetworkForm(action='/auth/prefs/add_social_network')
+
+    @LazyProperty 
+    def remove_socialnetwork_form(self):
+        '''
+        :return: None, or an easywidgets Form to render on  the user preferences page to 
+                 allow removing a social network account.
+        '''
+        from allura.lib.widgets.forms import RemoveSocialNetworkForm
+        return RemoveSocialNetworkForm(action='/auth/prefs/remove_social_network')
+
+    @LazyProperty 
+    def add_timeslot_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page
+                 to allow creating a new availability timeslot
+        ''' 
+        from allura.lib.widgets.forms import AddTimeSlotForm 
+        return AddTimeSlotForm()
+
+    @LazyProperty 
+    def remove_timeslot_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page
+                 to remove a timeslot
+        ''' 
+        from allura.lib.widgets.forms import RemoveTimeSlotForm 
+        return RemoveTimeSlotForm()
+
+    @LazyProperty 
+    def add_inactive_period_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page
+                 to allow creating a new period of inactivity
+        ''' 
+        from allura.lib.widgets.forms import AddInactivePeriodForm
+        return AddInactivePeriodForm()
+
+    @LazyProperty 
+    def remove_inactive_period_form(self): 
+        ''' 
+        :return: None, or an easywidgets Form to render on the user preferences page
+                 to allow removing an existing period of inactivity
+        ''' 
+        from allura.lib.widgets.forms import RemoveInactivePeriodForm
+        return RemoveInactivePeriodForm()
+
+    @LazyProperty
+    def add_trove_category(self):
+        '''
+        :return: None, or an easywidgets Form to render on the page to create a
+                 new trove_category
+        '''
+        from allura.lib.widgets.forms import AddTroveCategoryForm
+        return AddTroveCategoryForm(action='/categories/create')
+
+    @LazyProperty
+    def remove_trove_category(self):
+        '''
+        :return: None, or an easywidgets Form to render on the page to remove 
+                 an existing trove_category
+        '''
+        from allura.lib.widgets.forms import RemoveTroveCategoryForm
+        return RemoveTroveCategoryForm(action='/categories/remove')
+
+    @LazyProperty
+    def add_user_skill(self):
+        '''
+        :return: None, or an easywidgets Form to render on the page to add a
+                 new skill to a user profile
+        '''
+        from allura.lib.widgets.forms import AddUserSkillForm
+        return AddUserSkillForm(action='/auth/prefs/user_skills/save_skill')
+
+    @LazyProperty
+    def select_subcategory_form(self):
+        '''
+        :return: None, or an easywidgets Form to render on the page to add a
+                 new skill to a user profile, allowing to select a category in
+                 order to see its sub-categories
+        '''
+        from allura.lib.widgets.forms import SelectSubCategoryForm
+        return SelectSubCategoryForm(action='/auth/prefs/user_skills')
+
+    @LazyProperty
+    def remove_user_skill(self):
+        '''
+        :return: None, or an easywidgets Form to render on the page to remove
+                 an existing skill from a user profile
+        '''
+        from allura.lib.widgets.forms import RemoveSkillForm
+        return RemoveSkillForm(action='/auth/prefs/user_skills/remove_skill')
+
     @LazyProperty
     def upload_key_form(self):
         '''

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/lib/validators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index 5bab308..b7ff459 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -3,6 +3,7 @@ from bson import ObjectId
 import formencode as fe
 from formencode import validators as fev
 from . import helpers as h
+from datetime import datetime
 
 class Ming(fev.FancyValidator):
 
@@ -61,3 +62,86 @@ class JsonValidator(fev.FancyValidator):
         except ValueError, e:
             raise fe.Invalid('Invalid JSON: ' + str(e), value, state)
         return value
+
+class DateValidator(fev.FancyValidator):
+    def _to_python(self, value, state):
+        value = convertDate(value)
+        if not value:
+            raise fe.Invalid(
+                "Please enter a valid date in the format DD/MM/YYYY.",
+                value, state)
+        return value
+
+class TimeValidator(fev.FancyValidator):
+    def _to_python(self, value, state):
+        value = convertTime(value)
+        if not value:
+            raise fe.Invalid(
+                "Please enter a valid time in the format HH:MM.",
+                value, state)
+        return value
+
+class OneOfValidator(fev.FancyValidator):
+    def __init__(self, validvalues, not_empty = True):
+        self.validvalues = validvalues
+        self.not_empty = not_empty
+        super(OneOfValidator, self).__init__()
+
+    def _to_python(self, value, state):
+        if not value.strip():
+            if self.not_empty:
+                raise fe.Invalid("This field can't be empty.", value, state)
+            else:
+                return None
+        if not value in self.validvalues:
+            allowed = ''
+            for v in self.validvalues:
+                if allowed != '':
+                    allowed = allowed + ', '
+                allowed = allowed + '"%s"' % v
+            raise fe.Invalid(
+                "Invalid value. The allowed values are %s." %allowed,
+                value, state)
+        return value
+
+class MapValidator(fev.FancyValidator):
+    def __init__(self, mapvalues, not_empty = True):
+        self.map = mapvalues
+        self.not_empty = not_empty
+        super(MapValidator, self).__init__()
+
+    def _to_python(self, value, state):
+        if not value.strip():
+            if self.not_empty:
+                raise fe.Invalid("This field can't be empty.", value, state)
+            else:
+                return None
+        conv_value = self.map.get(value)
+        if not conv_value:
+            raise fe.Invalid(
+                "Invalid value. Please, choose one of the valid values.",
+                value, state)
+        return conv_value
+
+def convertDate(datestring):
+    formats = ['%Y-%m-%d', '%Y.%m.%d', '%Y/%m/%d', '%Y\%m\%d', '%Y %m %d',
+               '%d-%m-%Y', '%d.%m.%Y', '%d/%m/%Y', '%d\%m\%Y', '%d %m %Y']
+
+    for f in formats:
+        try:
+            date = datetime.strptime(datestring, f)       
+            return date
+        except:
+            pass
+    return None
+
+def convertTime(timestring):
+    formats = ['%H:%M', '%H.%M', '%H %M', '%H,%M']
+
+    for f in formats:
+        try:
+            time = datetime.strptime(timestring, f)       
+            return {'h':time.hour, 'm':time.minute}
+        except:
+            pass
+    return None

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/lib/widgets/forms.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index 35281d1..5fe3923 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -6,6 +6,7 @@ from allura.lib import helpers as h
 from allura.lib import plugin
 from allura.lib.widgets import form_fields as ffw
 from allura import model as M
+from datetime import datetime
 
 from formencode import validators as fev
 import formencode
@@ -13,8 +14,20 @@ import formencode
 import ew as ew_core
 import ew.jinja2_ew as ew
 
+from pytz import common_timezones, country_timezones, country_names
+
 log = logging.getLogger(__name__)
 
+socialnetworks=['Facebook','Linkedin','Twitter','Google+']
+weekdays=['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
+
+class _HTMLExplanation(ew.InputField):
+    template=ew.Snippet(
+        '''<label class="grid-4">&nbsp;</label>
+           <div class="grid-14" style="margin:2px;">{{widget.text}}</div>
+        ''',
+        'jinja2')
+
 class NeighborhoodProjectTakenValidator(fev.FancyValidator):
 
     def _to_python(self, value, state):
@@ -88,6 +101,517 @@ class PasswordChangeForm(ForgeForm):
             raise formencode.Invalid('Passwords must match', value, state)
         return d
 
+class PersonalDataForm(ForgeForm):
+    class fields(ew_core.NameList):
+        sex = ew.SingleSelectField(
+            label='Gender', 
+            options=[ew.Option(py_value=v,label=v,selected=False)
+                     for v in ['Male', 'Female', 'Unknown', 'Other']],
+            validator=formencode.All(
+                V.OneOfValidator(['Male', 'Female', 'Unknown', 'Other']),
+                fev.UnicodeString(not_empty=True)))
+        birthdate = ew.TextField(
+            label='Birth date', 
+            validator=V.DateValidator(),
+            attrs=dict(value=None))
+        exp = _HTMLExplanation(
+            text="Use the format DD/MM/YYYY",
+            show_errors=False)
+        country = ew.SingleSelectField(
+            label='Country of residence', 
+            validator=V.MapValidator(country_names, not_empty=False),
+            options = [
+                ew.Option(
+                    py_value=" ", label=" -- Unknown -- ", selected=False)] +\
+                [ew.Option(py_value=c, label=n, selected=False)
+                 for c,n in sorted(country_names.items(), 
+                                   key=lambda (k,v):v)],
+            attrs={'onchange':'selectTimezone(this.value)'})
+        city = ew.TextField(
+            label='City of residence', 
+            attrs=dict(value=None),
+            validator=fev.UnicodeString(not_empty=False))
+        timezone=ew.SingleSelectField(
+            label='Timezone', 
+            attrs={'id':'tz'},
+            validator=V.OneOfValidator(common_timezones, not_empty=False),
+            options=[
+                 ew.Option(
+                     py_value=" ",
+                     label=" -- Unknown -- ")] + \
+                 [ew.Option(py_value=n, label=n)
+                  for n in sorted(common_timezones)])
+
+    def display(self, **kw):
+        user = kw.get('user')
+
+        for opt in self.fields['sex'].options:
+            if opt.label == user.sex:
+                opt.selected = True
+            else:
+                opt.selected = False
+
+        if user.get_pref('birthdate'):
+            self.fields['birthdate'].attrs['value'] = \
+                user.get_pref('birthdate').strftime('%d/%m/%Y')
+        else:
+            self.fields['birthdate'].attrs['value'] = ''
+
+        for opt in self.fields['country'].options:
+            if opt.label == user.localization.country:
+                opt.selected = True
+            elif opt.py_value == " " and user.localization.country is None:
+                opt.selected = True
+            else:
+                opt.selected = False
+
+        if user.localization.city:
+            self.fields['city'].attrs['value'] = user.localization.city
+        else:
+            self.fields['city'].attrs['value'] = ''
+
+        for opt in self.fields['timezone'].options:
+            if opt.label == user.timezone:
+                opt.selected = True
+            elif opt.py_value == " " and user.timezone is None:
+                opt.selected = True
+            else:
+                opt.selected = False
+
+        return super(ForgeForm, self).display(**kw)
+        
+    def resources(self):
+        def _append(x, y):
+            return x + y
+
+        yield ew.JSScript('''
+var $allTimezones = $("#tz").clone();
+var $t = {};
+''' + \
+    reduce(_append, [
+        '$t["'+ el +'"] = ' + str([name.encode('utf-8') 
+                                  for name in country_timezones[el]]) + ";\n"
+        for el in country_timezones]) + '''
+function selectTimezone($country){
+     if($country == " "){
+         $("#tz").replaceWith($allTimezones);
+     }
+     else{
+         $("#tz option:gt(0)").remove();
+         $.each($t[$country], function(index, value){
+             $("#tz").append($("<option></option>").attr("value", value).text(value))
+         })
+     }
+}''')
+
+class AddTelNumberForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        newnumber = ew.TextField(
+            label='New telephone number',
+            attrs={'value':''},
+            validator=fev.UnicodeString(not_empty=True))
+        
+    def display(self, **kw):
+        initial_value = kw.get('initial_value','')
+        self.fields['newnumber'].attrs['value'] = initial_value
+        return super(ForgeForm, self).display(**kw)
+
+class AddWebsiteForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        newwebsite = ew.TextField(
+            label='New website url',
+            attrs={'value':''},
+            validator=fev.UnicodeString(not_empty=True))
+        
+    def display(self, **kw):
+        initial_value = kw.get('initial_value','')
+        self.fields['newwebsite'].attrs['value'] = initial_value
+        return super(ForgeForm, self).display(**kw)
+
+class SkypeAccountForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        skypeaccount = ew.TextField(
+            label='Skype account',
+            attrs={'value':''},
+            validator=fev.UnicodeString(not_empty=False))
+        
+    def display(self, **kw):
+        initial_value = kw.get('initial_value','')
+        self.fields['skypeaccount'].attrs['value'] = initial_value
+        return super(ForgeForm, self).display(**kw)
+
+class RemoveTextValueForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        initial_value = kw.get('value','')
+        label = kw.get('label','')
+        description = kw.get('description')
+        
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="oldvalue",
+                        attrs={'value':initial_value},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=label,
+                        show_errors=False),
+                    ew.HTMLField(
+                        show_label=False,
+                        text=initial_value),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Remove'},
+                        show_errors=False)])]
+        if description:
+            self.fields.append(
+                _HTMLExplanation(
+                    text=description,
+                    show_errors=False))
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveTextValueForm, self).to_python(kw, state)
+        d["oldvalue"] = kw.get('oldvalue', '')
+        return d
+
+class AddSocialNetworkForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        socialnetwork = ew.SingleSelectField(
+            label='Social network', 
+            options=[ew.Option(py_value=name, label=name)
+                     for name in socialnetworks],
+            validator=formencode.All(
+                V.OneOfValidator(socialnetworks),
+                fev.UnicodeString(not_empty=True)))
+        accounturl = ew.TextField(
+            label='Account url',
+            validator=fev.UnicodeString(not_empty=True))
+
+class RemoveSocialNetworkForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        account = kw.get('account','')
+        socialnetwork = kw.get('socialnetwork','')
+        
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="account",
+                        attrs={'value':account},
+                        show_errors=False),
+                    ew.HiddenField(
+                        name="socialnetwork",
+                        attrs={'value':socialnetwork},
+                        show_errors=False)],
+                fields=[
+                    ew.HTMLField(
+                        text='%s account' % socialnetwork,
+                        show_errors=False),
+                    ew.HTMLField(
+                        show_label=False,
+                        text=account),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Remove'},
+                        show_errors=False)])]
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveSocialNetworkForm, self).to_python(kw, state)
+        d["account"] = kw.get('account', '')
+        d["socialnetwork"] = kw.get('socialnetwork', '')
+        return d
+
+class AddInactivePeriodForm(ForgeForm):
+    class fields(ew_core.NameList):
+        startdate = ew.TextField(
+            label='Start date',
+            validator=formencode.All(
+                V.DateValidator(),
+                fev.UnicodeString(not_empty=True)))
+        enddate = ew.TextField(
+            label='End date',
+            validator=formencode.All(
+                V.DateValidator(),
+                fev.UnicodeString(not_empty=True)))
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(AddInactivePeriodForm, self).to_python(kw, state)
+        if d['startdate'] > d['enddate']:
+                raise formencode.Invalid(
+                   'Invalid period: start date greater than end date.', 
+                    kw, state)
+        return d
+
+class RemoveInactivePeriodForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        startdate = kw.get('startdate')
+        enddate = kw.get('enddate')
+
+        self.fields = [
+            ew.RowField(
+                show_label=False,
+                show_errors=False,
+                fields=[
+                    ew.HTMLField(text=startdate.strftime('%d/%m/%Y'),
+                                 show_errors=False),
+                    ew.HTMLField(text=enddate.strftime('%d/%m/%Y'),
+                                 show_errors=False),
+                    ew.SubmitButton(
+                        attrs={'value':'Remove'},
+                        show_errors=False)],
+                hidden_fields=[
+                    ew.HiddenField(
+                        name='startdate',
+                        attrs={'value':startdate.strftime('%d/%m/%Y')},
+                        show_errors=False),
+                    ew.HiddenField(
+                        name='enddate',
+                        attrs={'value':enddate.strftime('%d/%m/%Y')},
+                        show_errors=False)])]
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveInactivePeriodForm, self).to_python(kw, state)
+        d['startdate'] = V.convertDate(kw.get('startdate',''))
+        d['enddate'] = V.convertDate(kw.get('enddate',''))
+        return d
+
+class AddTimeSlotForm(ForgeForm):
+    class fields(ew_core.NameList):
+        weekday = ew.SingleSelectField(
+            label='Weekday', 
+            options=[ew.Option(py_value=wd, label=wd)
+                     for wd in weekdays],
+            validator=formencode.All(
+                V.OneOfValidator(weekdays),
+                fev.UnicodeString(not_empty=True)))
+        starttime = ew.TextField(
+            label='Start time',
+            validator=formencode.All(
+                V.TimeValidator(),
+                fev.UnicodeString(not_empty=True)))
+        endtime = ew.TextField(
+            label='End time',
+            validator=formencode.All(
+                V.TimeValidator(),
+                fev.UnicodeString(not_empty=True)))
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(AddTimeSlotForm, self).to_python(kw, state)
+        if (d['starttime']['h'], d['starttime']['m']) > \
+           (d['endtime']['h'], d['endtime']['m']):
+                raise formencode.Invalid(
+                   'Invalid period: start time greater than end time.', 
+                    kw, state)
+        return d
+
+class RemoveTimeSlotForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        weekday = kw.get('weekday','')
+        starttime = kw.get('starttime')
+        endtime = kw.get('endtime')
+
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                show_label=False,
+                fields=[
+                    ew.HTMLField(text=weekday),
+                    ew.HTMLField(text=starttime.strftime('%H:%M')),
+                    ew.HTMLField(text=endtime.strftime('%H:%M')),
+                    ew.SubmitButton(
+                        show_errors=False,
+                        attrs={'value':'Remove'})],
+                hidden_fields=[
+                    ew.HiddenField(
+                        name='weekday', 
+                        attrs={'value':weekday}),
+                    ew.HiddenField(
+                        name='starttime',
+                        attrs={'value':starttime.strftime('%H:%M')}),
+                    ew.HiddenField(
+                        name='endtime',
+                        attrs={'value':endtime.strftime('%H:%M')})])]
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveTimeSlotForm, self).to_python(kw, state)
+        d["weekday"] = kw.get('weekday', None)
+        d['starttime'] = V.convertTime(kw.get('starttime',''))
+        d['endtime'] = V.convertTime(kw.get('endtime',''))
+        return d
+
+class RemoveTroveCategoryForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        cat = kw.get('category')
+        
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                show_label=False,
+                fields=[
+                    ew.LinkField(
+                        text=cat.fullname,
+                        href="/categories/%s" % cat.shortname),
+                    ew.SubmitButton(
+                        show_errors=False,
+                        attrs={'value':'Remove'})],
+                hidden_fields=[
+                    ew.HiddenField(
+                        name='categoryid', 
+                        attrs={'value':cat.trove_cat_id})])]
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveTroveCategoryForm, self).to_python(kw, state)
+        d["categoryid"] = kw.get('categoryid')
+        if d["categoryid"]:
+            d["categoryid"] = int(d['categoryid'])
+        return d
+
+class AddTroveCategoryForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        uppercategory_id = ew.HiddenField(
+            attrs={'value':''},
+            show_errors=False)
+        categoryname = ew.TextField(
+            label="Category name",
+            validator=fev.UnicodeString(not_empty=True))
+
+    def display(self, **kw):
+        upper_category = kw.get('uppercategory_id',0)
+        
+        self.fields['uppercategory_id'].attrs['value'] = upper_category
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(AddTroveCategoryForm, self).to_python(kw, state)
+        d["uppercategory_id"] = kw.get('uppercategory_id', 0)
+        return d
+
+class AddUserSkillForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults)
+
+    class fields(ew_core.NameList):
+        selected_skill=ew.HiddenField(
+            attrs={'value':''},
+            show_errors=False,
+            validator=fev.UnicodeString(not_empty=True))
+        level=ew.SingleSelectField(
+            label="Level of knowledge",
+            options=[
+                ew.Option(py_value="low",label="Low level"),
+                ew.Option(py_value="medium",label="Medium level"),
+                ew.Option(py_value="high",label="Advanced level")],
+            validator=formencode.All(
+                V.OneOfValidator(['low','medium','high']),
+                fev.UnicodeString(not_empty=True)))
+        comment=ew.TextArea(
+            label="Additional comments",
+            validator=fev.UnicodeString(not_empty=False),
+            attrs={'rows':5,'cols':30})
+
+    def display(self, **kw):
+        category = kw.get('selected_skill')
+        
+        self.fields["selected_skill"].attrs['value']=category
+        return super(ForgeForm, self).display(**kw)
+
+class SelectSubCategoryForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text="Confirm")
+
+    class fields(ew_core.NameList):
+        selected_category=ew.SingleSelectField(
+            name="selected_category",
+            label="Available categories",
+            options=[])
+
+    def display(self, **kw):
+        categories = kw.get('categories')
+        
+        self.fields['selected_category'].options= \
+            [ew.Option(py_value=el.trove_cat_id,label=el.fullname)
+             for el in categories]
+        self.fields['selected_category'].validator= \
+            validator=formencode.All(
+                V.OneOfValidator(categories),
+                fev.UnicodeString(not_empty=True))
+        return super(ForgeForm, self).display(**kw)
+
+class RemoveSkillForm(ForgeForm):
+    defaults=dict(ForgeForm.defaults, submit_text=None, show_errors=False)
+
+    def display(self, **kw):
+        skill = kw.get('skill')
+        comment = skill['comment']
+        if not comment:
+            comment = "&nbsp;"
+
+        self.fields = [
+            ew.RowField(
+                show_errors=False,
+                hidden_fields=[
+                    ew.HiddenField(
+                        name="categoryid",
+                        attrs={'value':skill['skill'].trove_cat_id},
+                        show_errors=False)
+                ],
+                fields=[
+                    ew.HTMLField(
+                        text=skill['skill'].fullpath,
+                        show_errors=False),
+                    ew.HTMLField(
+                        text=skill['level'],
+                        show_errors=False),
+                    ew.HTMLField(
+                        text=comment,
+                        show_errors=False),
+                    ew.SubmitButton(
+                        show_label=False,
+                        attrs={'value':'Remove'},
+                        show_errors=False)])]
+        return super(ForgeForm, self).display(**kw)
+
+    @ew_core.core.validator
+    def to_python(self, kw, state):
+        d = super(RemoveSkillForm, self).to_python(kw, state)
+        d["categoryid"] = kw.get('categoryid', None)
+        return d
+
 class UploadKeyForm(ForgeForm):
     class fields(ew_core.NameList):
         key = ew.TextArea(label='SSH Public Key')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 349e05f..c1ff1f4 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -5,11 +5,13 @@ import logging
 import urllib
 import hmac
 import hashlib
+import pytz
 from urlparse import urlparse
 from email import header
-from datetime import timedelta, datetime
 from hashlib import sha256
 import uuid
+from pytz import timezone
+from datetime import timedelta, date, datetime, time
 
 import iso8601
 import pymongo
@@ -299,6 +301,35 @@ class User(MappedClass, ActivityNode, ActivityObject):
             email_address=str,
             email_format=str))
 
+    #Personal data
+    sex=FieldProperty(
+        S.OneOf('Male', 'Female', 'Other', 'Unknown', 
+        if_missing='Unknown'))
+    birthdate=FieldProperty(S.DateTime, if_missing=None)
+
+    #Availability information
+    availability=FieldProperty([dict(
+        week_day=str,
+        start_time=dict(h=int, m=int),
+        end_time=dict(h=int, m=int))])
+    localization=FieldProperty(dict(city=str,country=str))
+    timezone=FieldProperty(str)
+    inactiveperiod=FieldProperty([dict(
+        start_date=S.DateTime,
+        end_date=S.DateTime)])
+
+    #Additional contacts
+    socialnetworks=FieldProperty([dict(socialnetwork=str,accounturl=str)])
+    telnumbers=FieldProperty([str])
+    skypeaccount=FieldProperty(str)
+    webpages=FieldProperty([str])
+
+    #Skills list
+    skills = FieldProperty([dict(
+        category_id = S.ObjectId,
+        level = S.OneOf('low', 'high', 'medium'),
+        comment=str)])
+
     @property
     def activity_name(self):
         return self.display_name or self.username
@@ -309,6 +340,139 @@ 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 add_socialnetwork(self, socialnetwork, accounturl):
+        self.socialnetworks.append(dict(
+            socialnetwork=socialnetwork,
+            accounturl=accounturl))
+
+    def remove_socialnetwork(self, socialnetwork, oldurl):
+        for el in self.socialnetworks:
+            if el.socialnetwork==socialnetwork and el.accounturl==oldurl:
+                del self.socialnetworks[self.socialnetworks.index(el)]
+                return
+
+    def add_telephonenumber(self, telnumber):
+        self.telnumbers.append(telnumber)
+
+    def remove_telephonenumber(self, oldvalue):
+        for el in self.telnumbers:
+            if el==oldvalue:
+                del self.telnumbers[self.telnumbers.index(el)]
+                return
+
+    def add_webpage(self, webpage):
+        self.webpages.append(webpage)
+
+    def remove_webpage(self, oldvalue):
+        for el in self.webpages:
+            if el==oldvalue:
+                del self.webpages[self.webpages.index(el)]
+                return
+
+    def add_timeslot(self, weekday, starttime, endtime):
+        self.availability.append(
+           dict(week_day=weekday, 
+                start_time=starttime, 
+                end_time=endtime))
+
+    def remove_timeslot(self, weekday, starttime, endtime):
+        oldel = dict(week_day=weekday, start_time=starttime, end_time=endtime)
+        for el in self.availability:
+            if el == oldel:
+                del self.availability[self.availability.index(el)]
+                return
+
+    def add_inactive_period(self, startdate, enddate):
+        self.inactiveperiod.append(
+           dict(start_date=startdate, 
+                end_date=enddate))
+
+    def remove_inactive_period(self, startdate, enddate):
+        oldel = dict(start_date=startdate, end_date=enddate)
+        for el in self.inactiveperiod:
+            if el == oldel:
+                del self.inactiveperiod[self.inactiveperiod.index(el)]
+                return
+
+    def get_localized_availability(self, tz_name):
+        week_day = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 
+                    'Friday', 'Saturday', 'Sunday']
+        avail = self.get_availability_timeslots()
+        usertimezone = timezone(self.get_pref('timezone'))
+        chosentimezone = timezone(tz_name)
+        retlist = []
+        for t in avail:
+            today = datetime.today()
+            start = datetime(
+                today.year, today.month, today.day, 
+                t['start_time'].hour, t['start_time'].minute, 0)
+            end = datetime(
+                today.year, today.month, today.day, 
+                t['end_time'].hour, t['end_time'].minute, 0)            
+
+            loctime1 = usertimezone.localize(start)
+            loctime2 = usertimezone.localize(end)
+            convtime1 = loctime1.astimezone(chosentimezone)           
+            convtime2 = loctime2.astimezone(chosentimezone)
+
+            dif_days_start = convtime1.weekday() - today.weekday()
+            dif_days_end = convtime2.weekday() - today.weekday()
+            index = (week_day.index(t['week_day'])+dif_days_start) % 7
+            week_day_start = week_day[index]
+            week_day_end = week_day[index]
+
+            if week_day_start == week_day_end:
+                retlist.append(dict(
+                    week_day = week_day_start,
+                    start_time = convtime1.time(),
+                    end_time = convtime2.time()))
+            else:
+                retlist.append(dict(
+                    week_day = week_day_start,
+                    start_time = convtime1.time(),
+                    end_time = time(23, 59)))
+                retlist.append(dict(
+                    week_day = week_day_end,
+                    start_time = time(0, 0),
+                    end_time = convtime2.time()))
+
+        return sorted(
+            retlist, 
+            key=lambda k:(week_day.index(k['week_day']), k['start_time']))
+
+    def get_skills(self):
+        from allura.model.project import TroveCategory
+        retval = []
+        for el in self.skills:
+            d = dict(
+                skill=TroveCategory.query.get(_id=el["category_id"]),
+                level=el.level,
+                comment=el.comment)
+            retval.append(d)
+        return retval
+
+    def get_availability_timeslots(self):
+        retval = []
+        for el in self.availability:
+            start, end = (el.get('start_time'), el.get('end_time'))
+            (starth, startm) = (start.get('h'), start.get('m'))
+            (endh, endm) = (end.get('h'), end.get('m')) 
+            newdict = dict(
+                week_day  = el.get('week_day'),
+                start_time= time(starth,startm,0),
+                end_time  = time(endh,endm,0))
+            retval.append(newdict) 
+        return retval
+
+    def get_inactive_periods(self, include_past_periods=False):
+        retval = []
+        for el in self.inactiveperiod:
+            d1, d2 = (el.get('start_date'), el.get('end_date'))
+            newdict = dict(start_date = d1, end_date = d2)
+            if include_past_periods or newdict['end_date'] > datetime.today():
+                retval.append(newdict) 
+        return retval
+
     def url(self):
         return plugin.AuthenticationProvider.get(request).project_url(self)
 
@@ -464,6 +628,10 @@ class User(MappedClass, ActivityNode, ActivityObject):
     def update_notifications(self):
         return plugin.AuthenticationProvider.get(request).update_notifications(self)
 
+    @classmethod
+    def withskill(cls, skill):
+        return cls.query.find({"skills.category_id" : skill._id})
+
 class OldProjectRole(MappedClass):
     class __mongometa__:
         session = project_orm_session

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index b5c1567..d4e53ac 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -69,6 +69,7 @@ class TroveCategory(MappedClass):
     fullname = FieldProperty(str, if_missing='')
     fullpath = FieldProperty(str, if_missing='')
     parent_only = FieldProperty(bool, if_missing=False)
+    show_as_skill = FieldProperty(bool, if_missing=True)
 
     @property
     def parent_category(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/templates/trovecategories.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/trovecategories.html b/Allura/allura/templates/trovecategories.html
new file mode 100644
index 0000000..b97f20b
--- /dev/null
+++ b/Allura/allura/templates/trovecategories.html
@@ -0,0 +1,60 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}Trove categories{% endblock %}
+
+{% block header %}Managing trove categories{% endblock %}
+
+{% block content %}
+  <div class="grid-20">
+    {% if selected_cat %} 
+      <div class="grid-20">
+        <a href="/categories">Top-level categories</a>
+        {% for cat in hierarchy %}
+          &gt; <a href="/categories/{{cat.shortname}}">{{cat.fullname}}</a>
+        {% endfor %}
+        &gt; {{selected_cat.fullname}}
+      </div>
+      <h2>
+        Sub-categories of {{selected_cat.fullname}}
+      </h2>
+    {% else %} 
+      <h2>
+        List of all top-level categories
+      </h2>
+    {% endif %}
+   
+    {% if categories|length > 0 %}
+      <table>
+        <thead>
+          <tr>
+            <th>Name</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for cat in categories %}
+              {{g.theme.remove_trove_category.display(category=cat)}}
+          {% endfor %}
+        </tbody>
+      </table>
+    {% else %}
+      <div class="grid-20">
+        There are no categories in this list.
+      </div>
+    {% endif %}
+  </div>
+
+  <div class="grid-20">
+    <h2>Create a new item in this category</h2>
+    {% if selected_cat %}
+      {{g.theme.add_trove_category.display(uppercategory_id=selected_cat.trove_cat_id)}}
+    {% else %}
+      {{g.theme.add_trove_category.display(uppercategory_id=0)}}
+    {% endif %}
+    <div class="grid-20" style="margin-bottom:10px;">
+      Are you done creating new categories? <a href="/auth/prefs/user_skills/{{selected_cat.shortname}}">Click here</a> to configure your skills!
+    </div>
+
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
index 5d7ab23..be4dfe6 100644
--- a/Allura/allura/templates/user_preferences.html
+++ b/Allura/allura/templates/user_preferences.html
@@ -17,6 +17,118 @@
       {%- endfor %}
    </ul>
 
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Settings</h2>
+    {{g.theme.personal_data_form.display(action="/auth/prefs/change_personal_data", user=c.user)}} 
+  </div>
+
+  <div style="clear:both" class="grid-20">
+    <a name="Contacts"></a>
+    <h2>Personal Contacts</h2>
+    <h3>Skype account</h3>
+
+    {{g.theme.skype_account_form.display(action="/auth/prefs/skype_account",
+          initial_value=c.user.get_pref('skypeaccount'))}}
+     
+    {%if c.user.get_pref('socialnetworks') or c.user.get_pref('telnumbers') or c.user.get_pref('webpages') %}
+      <h3>Other existing contacts</h3>
+        <table>
+          <tr>
+            <thead>
+              <th>Type</th>
+              <th>Contact</th>
+              <th>Actions</th>
+            </thead>
+          </tr>
+          {% for sn in c.user.get_pref('socialnetworks') %}
+             {{g.theme.remove_socialnetwork_form.display(account=sn.accounturl, socialnetwork=sn.socialnetwork)}} 
+          {% endfor %}
+
+          {% for tn in c.user.get_pref('telnumbers') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_telnumber", value=tn, label="Telephone number")}} 
+          {%endfor%}
+
+          {% for ws in c.user.get_pref('webpages') %}
+              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/remove_webpage", value=ws, label="Website url")}} 
+          {%endfor%}
+        </table>
+    {% endif %}
+
+    <h3>Add a social network account</h3>
+    {{g.theme.add_socialnetwork_form.display(action="/auth/prefs/add_social_network")}}
+    <h3>Add a telephone number</h3>
+    {{g.theme.add_telnumber_form.display(action="/auth/prefs/add_telnumber")}}
+    <h3>Add a personal website</h3>
+    {{g.theme.add_website_form.display(action="/auth/prefs/add_webpage")}}
+  </div>
+
+  <a name="Availability"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Availability</h2>
+    <div class="grid-18">
+      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge.
+      Please, set your time intervals choosing a weekday and entering the time interval according to the timezone specified in your 
+      personal data, using the format HH:MM. If you didn't set any timezone, your timeslots could be meaningless to other users, 
+      therefore they will be ignored.
+    </div>
+    <div class="grid-18">
+      You can also specify periods of time during which you won't be able to work on the forge, in orther to communicate other users
+      that they can't contact you during those days. Please, do it specifying date intervals in format DD/MM/YYYY.
+    </div> 
+  </div>
+  <div class="grid-20">
+    {%if c.user.get_availability_timeslots() %}
+      <h3>Existing availability timeslots</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Weekday</th>
+            <th>Start time</th>
+            <th>End time</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ts in c.user.get_availability_timeslots() %}
+          {{g.theme.remove_timeslot_form.display(
+                action="/auth/prefs/remove_timeslot",
+                weekday=ts.week_day,
+                starttime=ts.start_time,
+                endtime=ts.end_time)}} 
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new availability timeslot</h3>
+    {{g.theme.add_timeslot_form.display(action="/auth/prefs/add_timeslot")}}
+  </div>
+
+  <div class="grid-20">
+    {%if c.user.get_inactive_periods() %}
+      <h3>Existing periods of inactivity on the forge</h3>
+      <table>
+        <tr>
+          <thead>
+            <th>Start date</th>
+            <th>End date</th>
+            <th>Actions</th>
+          </thead>
+        </tr>
+        {% for ip in c.user.get_inactive_periods() %}
+          {{g.theme.remove_inactive_period_form.display(
+                action="/auth/prefs/remove_inactive_period",
+                startdate=ip.start_date,
+                enddate=ip.end_date)}} 
+        {%endfor%}
+      </table>
+    {% endif %}
+    <h3>Add a new period of inactivity on the forge</h3>
+    {{g.theme.add_inactive_period_form.display(action="/auth/prefs/add_inactive_period")}}
+  </div>
+
+  <div class="grid-20">
+    <h2>Skills list</h2>
+    <ul><li><a href="/auth/prefs/user_skills">Click here to check and change your skills list</a></li></ul>
+  </div>
+
   {% if g.theme.password_change_form %}
   <div class="grid-20">
     <h2>Change Password</h2>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/templates/user_skills.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_skills.html b/Allura/allura/templates/user_skills.html
new file mode 100644
index 0000000..667ff70
--- /dev/null
+++ b/Allura/allura/templates/user_skills.html
@@ -0,0 +1,86 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Skills{% endblock %}
+
+{% block header %}Skills manager for {{c.user.username}} {% endblock %}
+
+{% block content %}
+  <div class="grid-20">
+    {% if c.user.get_skills()|length > 0 %}
+      <h2>Your current skills list:</h2>
+      <table>
+        <thead>
+          <tr>
+            <th>Skill</th>
+            <th>Level</th>
+            <th>Comments</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for s in c.user.get_skills() %}
+              {{g.theme.remove_user_skill.display(skill=s)}}
+          {% endfor %}
+        </tbody>
+      </table>
+    {% else %}
+      <h2>At the moment, your skills list is empty!</h2>
+      <div class="grid-20">
+        You can set your skills so that other users will be able to know what you can do best. 
+        To do it, you just need to choose the options that best fit your skills in the section below. You 
+        can also specify your skill level and some additional free comments.
+      </div>
+    {% endif %}
+  </div>
+ 
+  <div class="grid-20">
+    <h2>Add a new skill</h2>
+
+    {% if selected_skill %}
+      <div class="grid-20" style="margin:0;">
+        <div class="grid-4">
+          You selected:
+        </div>
+        <div class="grid-12" style="margin-bottom:20px">      
+           <a href="user_skills">List of all skills</a>
+           {% for cat in parents %}
+             &gt; <a href="/auth/prefs/user_skills/{{cat.shortname}}">{{cat.fullname}}</a>
+           {% endfor %}
+           &gt; <b>{{selected_skill.fullname}}</b>
+           <input type="hidden" name="upper_category" value="{{selected_skill.trove_parent_id}}"/>
+        </div>
+      </div>
+    {% endif %}
+ 
+    {% if skills_list %}
+      {% if selected_skill %}
+        <h3>Select a subcategory of "{{selected_skill.fullname}}"</h3>
+      {% else %}
+        <h3>Select a category</h3>
+      {%endif%}
+      {{g.theme.select_subcategory_form.display(categories=skills_list)}}
+    {% endif %}
+    {% if selected_skill %}
+      <h3>Add "{{selected_skill.fullname}}" to you set of skills</h3>
+      {{g.theme.add_user_skill.display(selected_skill=selected_skill.trove_cat_id, 
+           action="/auth/prefs/user_skills/" + selected_skill.shortname + "/save_skill")}}
+    {% endif %}
+    <h3>Other possible actions</h3>
+    <div class="grid-20" style="margin-bottom:10px;"/>
+      <ul>
+        {%if tg.config.get('trovecategories.enableediting', 'false')=='true'%}
+          <li>
+            <a href="/categories/{{selected_skill.shortname}}">
+              Create a new category in this list
+            </a>
+            if you want to add a more specific kind of skill which is not included here.
+          </li>
+        {%endif%}
+        <li>
+          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
+        </li>
+      </ul>
+    </div>
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/01c20a19/Allura/allura/templates/widgets/forge_form.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/widgets/forge_form.html b/Allura/allura/templates/widgets/forge_form.html
index a997522..6be3d86 100644
--- a/Allura/allura/templates/widgets/forge_form.html
+++ b/Allura/allura/templates/widgets/forge_form.html
@@ -24,12 +24,14 @@
       {{field.display(**ctx)}}
     {% endif %}
   {% endfor %}
-  <label class="grid-4">&nbsp;</label>
-  <div class="grid-{{15 + extra_width}}">
-    {% for b in buttons %}
-      {{b.display()}}
-    {% endfor %}
-  </div>
+  {% if buttons %}
+    <label class="grid-4">&nbsp;</label>
+    <div class="grid-{{15 + extra_width}}">
+      {% for b in buttons %}
+        {{b.display()}}
+      {% endfor %}
+    </div>
+  {% endif %}
   {% if widget.antispam %}{% for fld in g.antispam.extra_fields() %}
   {{fld}}{% endfor %}{% endif %}
 </form>