You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2013/03/21 15:21:40 UTC

[04/48] git commit: [#5909] Split Preferences and Personal Info off from Subscriptions page

[#5909] Split Preferences and Personal Info off from Subscriptions page

Signed-off-by: Cory Johns <jo...@geek.net>


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

Branch: refs/heads/si/5453
Commit: b55760a66e263adf4626ee8ff146e6a1b19fb149
Parents: 8feb541
Author: Cory Johns <jo...@geek.net>
Authored: Wed Mar 13 14:56:46 2013 +0000
Committer: Cory Johns <jo...@geek.net>
Committed: Wed Mar 13 20:20:46 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py                  |  309 +++++++++------
 Allura/allura/lib/plugin.py                        |   28 +-
 Allura/allura/nf/allura/css/site_style.css         |   77 ++++
 .../templates/jinja_master/theme_macros.html       |    4 +-
 Allura/allura/templates/oauth_authorize_ok.html    |    4 +-
 Allura/allura/templates/trovecategories.html       |    2 +-
 Allura/allura/templates/user_availability.html     |   45 ++-
 Allura/allura/templates/user_contacts.html         |   35 +-
 Allura/allura/templates/user_info.html             |   47 +++
 Allura/allura/templates/user_preferences.html      |  195 ---------
 Allura/allura/templates/user_prefs.html            |  140 +++++++
 Allura/allura/templates/user_skills.html           |   33 +-
 Allura/allura/templates/user_subs.html             |   54 +++
 Allura/allura/tests/functional/test_auth.py        |  100 +++---
 14 files changed, 632 insertions(+), 441 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 462f449..e52ea5e 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -62,7 +62,9 @@ class F(object):
 class AuthController(BaseController):
 
     def __init__(self):
-        self.prefs = PreferencesController()
+        self.preferences = PreferencesController()
+        self.user_info = UserInfoController()
+        self.subscriptions = SubscriptionsController()
         self.oauth = OAuthController()
 
     @expose('jinja:allura:templates/login.html')
@@ -188,7 +190,7 @@ class AuthController(BaseController):
         if c.user:
             c.user.claim_openid(oid_obj._id)
             flash('Claimed %s' % oid_obj._id)
-        redirect('/auth/prefs/')
+        redirect('/auth/preferences/')
 
     @expose()
     def logout(self):
@@ -293,21 +295,169 @@ class AuthController(BaseController):
                     allow_write=has_access(c.app, 'write')(user=user),
                     allow_create=has_access(c.app, 'create')(user=user))
 
+class PreferencesController(BaseController):
+
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/user_prefs.html')
+    def index(self, **kw):
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        api_token = M.ApiToken.query.get(user_id=c.user._id)
+        return dict(
+                menu=menu,
+                api_token=api_token,
+                authorized_applications=M.OAuthAccessToken.for_user(c.user),
+            )
+
+    @h.vardec
+    @expose()
+    @require_post()
+    def update(self,
+               addr=None,
+               new_addr=None,
+               primary_addr=None,
+               oid=None,
+               new_oid=None,
+               preferences=None,
+               **kw):
+        if config.get('auth.method', 'local') == 'local':
+            if not preferences.get('display_name'):
+                flash("Display Name cannot be empty.",'error')
+                redirect('.')
+            c.user.set_pref('display_name', preferences['display_name'])
+            for i, (old_a, data) in enumerate(zip(c.user.email_addresses, addr or [])):
+                obj = c.user.address_object(old_a)
+                if data.get('delete') or not obj:
+                    del c.user.email_addresses[i]
+                    if obj: obj.delete()
+            c.user.set_pref('email_address', primary_addr)
+            if new_addr.get('claim'):
+                if M.EmailAddress.query.get(_id=new_addr['addr'], confirmed=True):
+                    flash('Email address already claimed', 'error')
+                else:
+                    c.user.email_addresses.append(new_addr['addr'])
+                    em = M.EmailAddress.upsert(new_addr['addr'])
+                    em.claimed_by_user_id=c.user._id
+                    em.send_verification_link()
+            for i, (old_oid, data) in enumerate(zip(c.user.open_ids, oid or [])):
+                obj = c.user.openid_object(old_oid)
+                if data.get('delete') or not obj:
+                    del c.user.open_ids[i]
+                    if obj: obj.delete()
+            for k,v in preferences.iteritems():
+                if k == 'results_per_page':
+                    v = int(v)
+                c.user.set_pref(k, v)
+        redirect('.')
+
+    @expose()
+    @require_post()
+    def gen_api_token(self):
+        tok = M.ApiToken.query.get(user_id=c.user._id)
+        if tok is None:
+            tok = M.ApiToken(user_id=c.user._id)
+        else:
+            tok.secret_key = h.cryptographic_nonce()
+        redirect(request.referer)
+
+    @expose()
+    @require_post()
+    def del_api_token(self):
+        tok = M.ApiToken.query.get(user_id=c.user._id)
+        if tok is None: return
+        tok.delete()
+        redirect(request.referer)
+
+    @expose()
+    @require_post()
+    def revoke_oauth(self, _id=None):
+        tok = M.OAuthAccessToken.query.get(_id=bson.ObjectId(_id))
+        if tok is None:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        if tok.user_id != c.user._id:
+            flash('Invalid app ID', 'error')
+            redirect('.')
+        tok.delete()
+        flash('Application access revoked')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    @validate(V.NullValidator(), error_handler=index)
+    def change_password(self, **kw):
+        kw = g.theme.password_change_form.to_python(kw, None)
+        ap = plugin.AuthenticationProvider.get(request)
+        try:
+            ap.set_password(c.user, kw['oldpw'], kw['pw'])
+        except wexc.HTTPUnauthorized:
+            flash('Incorrect password', 'error')
+            redirect('.')
+        flash('Password changed')
+        redirect('.')
+
+    @expose()
+    @require_post()
+    def upload_sshkey(self, key=None):
+        ap = plugin.AuthenticationProvider.get(request)
+        try:
+            ap.upload_sshkey(c.user.username, key)
+        except AssertionError, ae:
+            flash('Error uploading key: %s' % ae, 'error')
+        flash('Key uploaded')
+        redirect('.')
+
+class UserInfoController(BaseController):
+
+    def __init__(self, *args, **kwargs):
+        self.skills = UserSkillsController()
+        self.contacts = UserContactsController()
+        self.availability = UserAvailabilityController()
+
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
+    @expose('jinja:allura:templates/user_info.html')
+    def index(self, **kw):
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
+
+    @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('.')
+
 class UserSkillsController(BaseController):
 
     def __init__(self, category=None):
         self.category = category
         super(UserSkillsController, self).__init__()
 
+    def _check_security(self):
+        require_authenticated()
+
     @expose()
     def _lookup(self, catshortname, *remainder):
         cat = M.TroveCategory.query.get(shortname=catshortname)
         return UserSkillsController(category=cat), remainder
 
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_skills.html')
     def index(self, **kw):
-        require_authenticated()
-
         l = []
         parents = []
         if kw.get('selected_category') is not None:
@@ -324,18 +474,19 @@ class UserSkillsController(BaseController):
             while temp_cat:
                 parents = [temp_cat] + parents
                 temp_cat = temp_cat.parent_category
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
         return dict(
             skills_list = l,
             selected_skill = selected_skill,
             parents = parents,
+            menu = menu,
             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)
 
@@ -344,34 +495,37 @@ class UserSkillsController(BaseController):
             level=kw.get('level'),
             comment=kw.get('comment'))
 
-        s = [skill for skill in c.user.skills 
+        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')
+        redirect('..')
 
     @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 
+        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')
+        redirect('..')
 
 class UserContactsController(BaseController):
 
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_contacts.html')
     def index(self, **kw):
-        require_authenticated()
-        return dict()
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
 
     @expose()
     @require_post()
@@ -438,10 +592,15 @@ class UserContactsController(BaseController):
 
 class UserAvailabilityController(BaseController):
 
+    def _check_security(self):
+        require_authenticated()
+
+    @with_trailing_slash
     @expose('jinja:allura:templates/user_availability.html')
     def index(self, **kw):
-        require_authenticated()
-        return dict()
+        provider = plugin.AuthenticationProvider.get(request)
+        menu = provider.account_navigation()
+        return dict(menu=menu)
 
     @expose()
     @require_post()
@@ -479,16 +638,14 @@ class UserAvailabilityController(BaseController):
         flash('Your availability timeslots were successfully updated!')
         redirect('.')
 
-class PreferencesController(BaseController):
+class SubscriptionsController(BaseController):
 
-    user_skills = UserSkillsController()
-    user_contacts = UserContactsController()
-    user_availability = UserAvailabilityController()
+    def _check_security(self):
+        require_authenticated()
 
     @with_trailing_slash
-    @expose('jinja:allura:templates/user_preferences.html')
+    @expose('jinja:allura:templates/user_subs.html')
     def index(self, **kw):
-        require_authenticated()
         c.form = F.subscription_form
         c.revoke_access = F.oauth_revocation_form
         subscriptions = []
@@ -541,12 +698,10 @@ class PreferencesController(BaseController):
                     frequency=None,
                     artifact=None))
         subscriptions.sort(key=lambda d: (d['project_name'], d['mount_point']))
-        api_token = M.ApiToken.query.get(user_id=c.user._id)
         provider = plugin.AuthenticationProvider.get(request)
         menu = provider.account_navigation()
         return dict(
             subscriptions=subscriptions,
-            api_token=api_token,
             authorized_applications=M.OAuthAccessToken.for_user(c.user),
             menu=menu)
 
@@ -554,43 +709,8 @@ class PreferencesController(BaseController):
     @expose()
     @require_post()
     def update(self,
-               display_name=None,
-               addr=None,
-               new_addr=None,
-               primary_addr=None,
-               oid=None,
-               new_oid=None,
                preferences=None,
                **kw):
-        require_authenticated()
-        if config.get('auth.method', 'local') == 'local':
-            if display_name is None:
-                flash("Display Name cannot be empty.",'error')
-                redirect('.')
-            c.user.set_pref('display_name', display_name)
-            for i, (old_a, data) in enumerate(zip(c.user.email_addresses, addr or [])):
-                obj = c.user.address_object(old_a)
-                if data.get('delete') or not obj:
-                    del c.user.email_addresses[i]
-                    if obj: obj.delete()
-            c.user.set_pref('email_address', primary_addr)
-            if new_addr.get('claim'):
-                if M.EmailAddress.query.get(_id=new_addr['addr'], confirmed=True):
-                    flash('Email address already claimed', 'error')
-                else:
-                    c.user.email_addresses.append(new_addr['addr'])
-                    em = M.EmailAddress.upsert(new_addr['addr'])
-                    em.claimed_by_user_id=c.user._id
-                    em.send_verification_link()
-            for i, (old_oid, data) in enumerate(zip(c.user.open_ids, oid or [])):
-                obj = c.user.openid_object(old_oid)
-                if data.get('delete') or not obj:
-                    del c.user.open_ids[i]
-                    if obj: obj.delete()
-            for k,v in preferences.iteritems():
-                if k == 'results_per_page':
-                    v = int(v)
-                c.user.set_pref(k, v)
         if 'email_format' in preferences:
             c.user.set_pref('email_format', preferences['email_format'])
         redirect('.')
@@ -611,77 +731,6 @@ class PreferencesController(BaseController):
                     s['subscription_id'].delete()
         redirect(request.referer)
 
-    @expose()
-    @require_post()
-    def gen_api_token(self):
-        tok = M.ApiToken.query.get(user_id=c.user._id)
-        if tok is None:
-            tok = M.ApiToken(user_id=c.user._id)
-        else:
-            tok.secret_key = h.cryptographic_nonce()
-        redirect(request.referer)
-
-    @expose()
-    @require_post()
-    def del_api_token(self):
-        tok = M.ApiToken.query.get(user_id=c.user._id)
-        if tok is None: return
-        tok.delete()
-        redirect(request.referer)
-
-    @expose()
-    @require_post()
-    def revoke_oauth(self, _id=None):
-        tok = M.OAuthAccessToken.query.get(_id=bson.ObjectId(_id))
-        if tok is None:
-            flash('Invalid app ID', 'error')
-            redirect('.')
-        if tok.user_id != c.user._id:
-            flash('Invalid app ID', 'error')
-            redirect('.')
-        tok.delete()
-        flash('Application access revoked')
-        redirect('.')
-
-    @expose()
-    @require_post()
-    @validate(V.NullValidator(), error_handler=index)
-    def change_password(self, **kw):
-        kw = g.theme.password_change_form.to_python(kw, None)
-        ap = plugin.AuthenticationProvider.get(request)
-        try:
-            ap.set_password(c.user, kw['oldpw'], kw['pw'])
-        except wexc.HTTPUnauthorized:
-            flash('Incorrect password', 'error')
-            redirect('.')
-        flash('Password changed')
-        redirect('.')
-
-    @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()
-    def upload_sshkey(self, key=None):
-        ap = plugin.AuthenticationProvider.get(request)
-        try:
-            ap.upload_sshkey(c.user.username, key)
-        except AssertionError, ae:
-            flash('Error uploading key: %s' % ae, 'error')
-        flash('Key uploaded')
-        redirect('.')
-
 class OAuthController(BaseController):
 
     @with_trailing_slash

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 3626167..3e33172 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -137,9 +137,21 @@ class AuthenticationProvider(object):
     def account_navigation(self):
         return [
             {
+                'tabid': 'account_user_prefs',
+                'title': 'Preferences',
+                'target': "/auth/preferences",
+                'alt': 'Manage Personal Preferences',
+            },
+            {
+                'tabid': 'account_user_info',
+                'title': 'Personal Info',
+                'target': "/auth/user_info",
+                'alt': 'Manage Personal Information',
+            },
+            {
                 'tabid': 'account_sfnet_beta_index',
                 'title': 'Subscriptions',
-                'target': "/auth/prefs",
+                'target': "/auth/subscriptions",
                 'alt': 'Manage Subscription Preferences',
             },
         ]
@@ -624,7 +636,7 @@ class ThemeProvider(object):
         :return: None, or an easywidgets Form to render on the user preferences page
         '''
         from allura.lib.widgets.forms import PasswordChangeForm
-        return PasswordChangeForm(action='/auth/prefs/change_password')
+        return PasswordChangeForm(action='/auth/preferences/change_password')
 
     @LazyProperty
     def personal_data_form(self):
@@ -677,7 +689,7 @@ class ThemeProvider(object):
                  allow adding a social network account.
         '''
         from allura.lib.widgets.forms import AddSocialNetworkForm
-        return AddSocialNetworkForm(action='/auth/prefs/add_social_network')
+        return AddSocialNetworkForm(action='/auth/preferences/add_social_network')
 
     @LazyProperty
     def remove_socialnetwork_form(self):
@@ -686,7 +698,7 @@ class ThemeProvider(object):
                  allow removing a social network account.
         '''
         from allura.lib.widgets.forms import RemoveSocialNetworkForm
-        return RemoveSocialNetworkForm(action='/auth/prefs/remove_social_network')
+        return RemoveSocialNetworkForm(action='/auth/preferences/remove_social_network')
 
     @LazyProperty
     def add_timeslot_form(self):
@@ -749,7 +761,7 @@ class ThemeProvider(object):
                  new skill to a user profile
         '''
         from allura.lib.widgets.forms import AddUserSkillForm
-        return AddUserSkillForm(action='/auth/prefs/user_skills/save_skill')
+        return AddUserSkillForm(action='/auth/user_info/skills/save_skill')
 
     @LazyProperty
     def select_subcategory_form(self):
@@ -759,7 +771,7 @@ class ThemeProvider(object):
                  order to see its sub-categories
         '''
         from allura.lib.widgets.forms import SelectSubCategoryForm
-        return SelectSubCategoryForm(action='/auth/prefs/user_skills')
+        return SelectSubCategoryForm(action='/auth/user_info/skills/')
 
     @LazyProperty
     def remove_user_skill(self):
@@ -768,7 +780,7 @@ class ThemeProvider(object):
                  an existing skill from a user profile
         '''
         from allura.lib.widgets.forms import RemoveSkillForm
-        return RemoveSkillForm(action='/auth/prefs/user_skills/remove_skill')
+        return RemoveSkillForm(action='/auth/user_info/skills/remove_skill')
 
     @LazyProperty
     def upload_key_form(self):
@@ -776,7 +788,7 @@ class ThemeProvider(object):
         :return: None, or an easywidgets Form to render on the user preferences page
         '''
         from allura.lib.widgets.forms import UploadKeyForm
-        return UploadKeyForm(action='/auth/prefs/upload_sshkey')
+        return UploadKeyForm(action='/auth/preferences/upload_sshkey')
 
     @property
     def master(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/nf/allura/css/site_style.css
----------------------------------------------------------------------
diff --git a/Allura/allura/nf/allura/css/site_style.css b/Allura/allura/nf/allura/css/site_style.css
index 1b28457..cc503f4 100644
--- a/Allura/allura/nf/allura/css/site_style.css
+++ b/Allura/allura/nf/allura/css/site_style.css
@@ -2801,3 +2801,80 @@ table thead tr th.narrow, table tr td.narrow {
 #selected-projects {
   padding: 10px 10px;
 }
+#account-nav-menu {
+  background-color: #e5e5e5;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  overflow: hidden;
+  *zoom: 1;
+  background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #cccccc));
+  background: -webkit-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -moz-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -o-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: -ms-linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  background: linear-gradient(top, #ffffff 0%, #cccccc 100%);
+  -moz-border-radius: 4px;
+  -webkit-border-radius: 4px;
+  -o-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -khtml-border-radius: 4px;
+  border-radius: 4px;
+  -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  -o-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  box-shadow: rgba(0, 0, 0, 0.4) 0 1px 3px inset, rgba(255, 255, 255, 0.9) 0 1px 0;
+  margin: 0 10px 1em;
+}
+#account-nav-menu li {
+  list-style-image: none;
+  list-style-type: none;
+  margin-left: 0;
+  display: -moz-inline-box;
+  -moz-box-orient: vertical;
+  display: inline-block;
+  vertical-align: middle;
+  *vertical-align: auto;
+  white-space: nowrap;
+}
+#account-nav-menu li {
+  *display: inline;
+}
+#account-nav-menu a {
+  display: -moz-inline-box;
+  -moz-box-orient: vertical;
+  display: inline-block;
+  vertical-align: middle;
+  *vertical-align: auto;
+  color: inherit;
+  text-decoration: inherit;
+  cursor: inherit;
+  cursor: pointer;
+  padding: 10px;
+  padding-bottom: 0;
+  color: #555;
+  font-family: Ubuntu, sans-serif;
+}
+#account-nav-menu a {
+  *display: inline;
+}
+#account-nav-menu a:active, #account-nav-menu a:focus {
+  outline: none;
+}
+#account-nav-menu .marker {
+  width: 10px;
+  height: 10px;
+}
+#account-nav-menu .marker.current {
+  -moz-transform: rotate(45deg);
+  -webkit-transform: rotate(45deg);
+  -o-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  transform: rotate(45deg);
+  background: white;
+  border-left: 1px solid #AAA;
+  border-top: 1px solid #AAA;
+  position: relative;
+  bottom: -5px;
+  left: 45%;
+}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/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 655b67b..d0d28ed 100644
--- a/Allura/allura/templates/jinja_master/theme_macros.html
+++ b/Allura/allura/templates/jinja_master/theme_macros.html
@@ -3,7 +3,7 @@
     <div class="wrapper">
         <nav>
           {% if c.user._id %}
-            <a href="/auth/prefs/">Account</a>
+            <a href="/auth/preferences/">Account</a>
             <a href="{{c.user.url()}}">{{name}}</a>
             <a href="{{logout_url}}">Log Out</a>
           {% else %}
@@ -34,7 +34,7 @@
     function _add_tracking(prefix, tracking_id) {
         _gaq.push(
             [prefix+'._setAccount', tracking_id],
-            [prefix+'._trackPageview'],
+            [prefix+'._trackPageview']
         );
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/oauth_authorize_ok.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/oauth_authorize_ok.html b/Allura/allura/templates/oauth_authorize_ok.html
index 33af743..5f02f54 100644
--- a/Allura/allura/templates/oauth_authorize_ok.html
+++ b/Allura/allura/templates/oauth_authorize_ok.html
@@ -8,8 +8,8 @@
 {% block content %}
 <p>You have authorized {{ rtok.consumer_token.name }} access to your account.  If you wish
   to revoke this access at any time, please visit 
-  <a href="{{g.url('/auth/prefs/') }}">user preferences</a>
+  <a href="{{g.url('/auth/preferences/') }}">user preferences</a>
   and click 'revoke access'.</p>
 <h2>PIN: {{ rtok.validation_pin }}</h2>
-<a href="{{g.url('/auth/prefs/')}}">Return to preferences</a>
+<a href="{{g.url('/auth/preferences/')}}">Return to preferences</a>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/trovecategories.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/trovecategories.html b/Allura/allura/templates/trovecategories.html
index b97f20b..ae1b1e5 100644
--- a/Allura/allura/templates/trovecategories.html
+++ b/Allura/allura/templates/trovecategories.html
@@ -53,7 +53,7 @@
       {{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!
+      Are you done creating new categories? <a href="/auth/user_info/skills/{{selected_cat.shortname}}">Click here</a> to configure your skills!
     </div>
 
   </div>

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_availability.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_availability.html b/Allura/allura/templates/user_availability.html
index d7f46b7..54778b7 100644
--- a/Allura/allura/templates/user_availability.html
+++ b/Allura/allura/templates/user_availability.html
@@ -6,15 +6,32 @@
 {% block header %}Availability timeslots of {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+  </ul>
+
   <div class="grid-20">
     <h2>Availability</h2>
     <div class="grid-18">
-      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.
+        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> 
+        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() %}
@@ -30,15 +47,15 @@
         </tr>
         {% for ts in c.user.get_availability_timeslots() %}
           {{g.theme.remove_timeslot_form.display(
-                action="/auth/prefs/user_availability/remove_timeslot",
+                action="remove_timeslot",
                 weekday=ts.week_day,
                 starttime=ts.start_time,
-                endtime=ts.end_time)}} 
+                endtime=ts.end_time)}}
         {%endfor%}
       </table>
     {% endif %}
     <h3>Add a new availability timeslot</h3>
-    {{g.theme.add_timeslot_form.display(action="/auth/prefs/user_availability/add_timeslot")}}
+    {{g.theme.add_timeslot_form.display(action="add_timeslot")}}
   </div>
 
   <div class="grid-20">
@@ -54,21 +71,13 @@
         </tr>
         {% for ip in c.user.get_inactive_periods() %}
           {{g.theme.remove_inactive_period_form.display(
-                action="/auth/prefs/user_availability/remove_inactive_period",
+                action="remove_inactive_period",
                 startdate=ip.start_date,
-                enddate=ip.end_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/user_availability/add_inactive_period")}}
-    <h3>Other possible actions</h3>
-    <div class="grid-20" style="margin-bottom:10px;"/>
-      <ul>
-        <li>
-          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
-        </li>
-      </ul>
-    </div>
+    {{g.theme.add_inactive_period_form.display(action="add_inactive_period")}}
   </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_contacts.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_contacts.html b/Allura/allura/templates/user_contacts.html
index d6d1811..9de0f24 100644
--- a/Allura/allura/templates/user_contacts.html
+++ b/Allura/allura/templates/user_contacts.html
@@ -6,13 +6,24 @@
 {% block header %}Contacts of {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
   <div class="grid-20">
     <h2>Personal Contacts</h2>
     <h3>Skype account</h3>
 
-    {{g.theme.skype_account_form.display(action="/auth/prefs/user_contacts/skype_account",
+    {{g.theme.skype_account_form.display(action="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>
@@ -24,32 +35,24 @@
             </thead>
           </tr>
           {% for sn in c.user.get_pref('socialnetworks') %}
-             {{g.theme.remove_socialnetwork_form.display(action="/auth/prefs/user_contacts/remove_social_network", account=sn.accounturl, socialnetwork=sn.socialnetwork)}} 
+             {{g.theme.remove_socialnetwork_form.display(action="remove_social_network", account=sn.accounturl, socialnetwork=sn.socialnetwork)}}
           {% endfor %}
 
           {% for tn in c.user.get_pref('telnumbers') %}
-              {{g.theme.remove_textvalue_form.display(action="/auth/prefs/user_contacts/remove_telnumber", value=tn, label="Telephone number")}} 
+              {{g.theme.remove_textvalue_form.display(action="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/user_contacts/remove_webpage", value=ws, label="Website url")}} 
+              {{g.theme.remove_textvalue_form.display(action="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/user_contacts/add_social_network")}}
+    {{g.theme.add_socialnetwork_form.display(action="add_social_network")}}
     <h3>Add a telephone number</h3>
-    {{g.theme.add_telnumber_form.display(action="/auth/prefs/user_contacts/add_telnumber")}}
+    {{g.theme.add_telnumber_form.display(action="add_telnumber")}}
     <h3>Add a personal website</h3>
-    {{g.theme.add_website_form.display(action="/auth/prefs/user_contacts/add_webpage")}}
-  <h3>Other possible actions</h3>
-    <div class="grid-20" style="margin-bottom:10px;"/>
-      <ul>
-        <li>
-          <a href="/auth/prefs">Go to you profile</a> to set the remaining personal preferences.
-        </li>
-      </ul>
-    </div>
+    {{g.theme.add_website_form.display(action="add_webpage")}}
   </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_info.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_info.html b/Allura/allura/templates/user_info.html
new file mode 100644
index 0000000..6fe112e
--- /dev/null
+++ b/Allura/allura/templates/user_info.html
@@ -0,0 +1,47 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Information</h2>
+    {{g.theme.personal_data_form.display(action="/auth/user_info/change_personal_data", user=c.user)}} 
+  </div>
+
+  <a name="Contacts"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Personal Contacts</h2>
+    <p>
+      If you want, you can set your contacts allowing other members to contact you or you can set your personal website allowing other members to introduce your self.
+    </p>
+    <ul><li><a href="contacts">Click here to check and change your contacts</a></li></ul>
+  </div>
+
+  <a name="Availability"></a>
+  <div style="clear:both" class="grid-20">
+    <h2>Availability</h2>
+    <p>
+      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge. (If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored!)
+    </p>
+    <ul><li><a href="availability">Click here to check and change your availability slots</a></li></ul>
+  </div>
+
+  <div class="grid-20">
+    <h2>Skills list</h2>
+    <ul><li><a href="skills">Click here to check and change your skills list</a></li></ul>
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_preferences.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_preferences.html b/Allura/allura/templates/user_preferences.html
deleted file mode 100644
index 4e1391e..0000000
--- a/Allura/allura/templates/user_preferences.html
+++ /dev/null
@@ -1,195 +0,0 @@
-{% set hide_left_bar = True %}
-{% extends g.theme.master %}
-
-{% block title %}{{c.user.username}} / Preferences{% endblock %}
-
-{% block header %}User Preferences for {{c.user.username}}{% endblock %}
-
-{% block content %}
-  <ul id="account-nav-menu" class="b-hornav droppy">
-      {% for item in menu -%}
-      <li id="{{ item.tabid }}">
-      <a href="{{ item.target }}">
-          {{ item.title }}
-          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
-      </a>
-      </li>
-      {%- 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>
-    <p>
-      If you want, you can set your contacts allowing other members to contact you or you can set your personal website allowing other members to introduce your self.
-    </p>
-    <ul><li><a href="/auth/prefs/user_contacts">Click here to check and change your contacts</a></li></ul>
-  </div>
-
-  <a name="Availability"></a>
-  <div style="clear:both" class="grid-20">
-    <h2>Availability</h2>
-    <p>
-      If you want, you can set the weekly timeslot during which you are usually available to support other users of the forge. (If you didn't set any timezone, your timeslots could be meaningless to other users, therefore they will be ignored!)
-    </p>
-    <ul><li><a href="/auth/prefs/user_availability">Click here to check and change your availability slots</a></li></ul>
-  </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>
-    {{ g.theme.password_change_form.display() }}
-  </div>
-  {% endif %}
-  {% if g.theme.upload_key_form %}
-  <div class="grid-20">
-    <h2>Upload ssh public key</h2>
-    {{ g.theme.upload_key_form.display() }}
-  </div>
-  {% endif %}
-
-  {% if tg.config.get('auth.method', 'local') == 'local' %}
-      <br style="clear:both"/>
-      <h2>API Token</h2>
-      {% if api_token %}
-        <p>
-          <b>API Key:</b><br/>
-          {{api_token.api_key}}<br/>
-          <b>Secret Key:</b><br/>
-          {{api_token.secret_key}}<br/>
-        </p>
-        <form method="POST" action="del_api_token" class="grid-18">
-          <input type="submit" value="Delete API Token">
-        </form>
-      {% else %}
-        <p>No API token generated</p>
-      {% endif %}
-      <form method="POST" action="gen_api_token" class="grid-18">
-        <input type="submit" value="(Re)generate API Token">
-      </form>
-  {% endif %}
-
-  <div style="clear:both"></div>
-
-  <h2>Authorized Third-party Applications</h2>
-  {% for access_tok in authorized_applications %}
-    <div>
-      <h3>{{access_tok.consumer_token.name}}</h3>
-      {{access_tok.consumer_token.description_html}}
-      {{ c.revoke_access.display(value=access_tok) }}
-      <br style="clear:both"/>
-  </div>
- {% endfor %}
-    {% if not authorized_applications %}<p>No authorized third-party applications</p>{% endif %}
-
-
-  <h2>Subscriptions</h2>
-  {% if subscriptions %}
-    <p><em>Mark tools that you want to subscribe to. Unmark tools that you want to unsubscribe from. Press 'Save' button.</em></p>
-    {{c.form.display(action='update_subscriptions', value=dict(subscriptions=subscriptions))}}
-  {% else%}
-    <p>No subscriptions.</p>
-  {% endif %}
-  <hr/>
-  <div style="clear:both">&nbsp;</div>
-  <form action="update" method="post">
-        {% if tg.config.get('auth.method', 'local') == 'local' %}
-        <label class="grid-4">Display Name</label>
-        <div class="grid-18">
-          <input name="preferences.display_name" value="{{c.user.display_name}}" type="text">
-        </div>
-        {% endif %}
-        <label class="grid-4">Email Format</label>
-        <div class="grid-18">
-          <select name="preferences.email_format">
-            <option value="plain" {{'selected' if c.user.preferences.email_format == 'plain' else ''}}>Plain Text</option>
-            <option value="html" {{'selected' if c.user.preferences.email_format == 'html' else ''}}>HTML</option>
-            <option value="both" {{'selected' if c.user.preferences.email_format == 'both' else ''}}>Combined</option>
-          </select>
-        </div>
-        {% if tg.config.get('auth.method', 'local') == 'local' %}
-        <label class="grid-4">Page Size</label>
-        <div class="grid-18">
-          <select name="preferences.results_per_page">
-            {% for per_page in [25, 50, 100, 250] %}
-                <option {% if per_page == c.user.preferences.results_per_page %}selected="selected"{% endif %}
-                   value="{{per_page}}">{{per_page}}</option>
-            {% endfor %}
-          </select>
-        </div>
-        {% endif %}
-
-    {% if tg.config.get('auth.method', 'local') == 'local' %}
-      {% for a in c.user.email_addresses %}
-        <input name="addr-{{loop.index0}}.ord" value="{{loop.index0}}" type="hidden"/>
-      {% endfor %}
-      {% if c.user.email_addresses %}
-        <h3 class="grid-18">Email Addresses</h3>
-        <table class="grid-18">
-          <tr>
-            <th>Primary?</th>
-            <th>Address</th>
-            <th>Confirmed</th>
-            <th></th>
-          </tr>
-          {% for a in c.user.email_addresses %}
-          <tr>
-            {% set obj = c.user.address_object(a) %}
-            <td>{{lib.radio_button('primary_addr', None, a, c.user.preferences.email_address)}}</td>
-            <td>{{a}}</td>
-            {% if obj %}
-            <td>
-              {% if obj.confirmed %}
-                yes
-              {% else %}
-                no (<a href="{{g.url('/auth/send_verification_link', a=a)}}">verify</a>)
-              {% endif %}
-            </td>
-            {% else %}
-              <td>Unknown addr obj {{a}}</td>
-            {% endif %}
-            <td>{{lib.submit_button('Delete', 'addr-%s.delete' % i)}}</td>
-          </tr>
-          {% endfor %}
-        </table>
-        {% endif %}
-        <div class="grid-18">
-        {{lib.text_field('new_addr.addr', 'New Email Address')}}
-        {{lib.submit_button('Claim Address', name='new_addr.claim')}}
-        </div>
-
-        {% if c.user.open_ids %}
-        <h3 class="grid-18">OpenIDs Claimed</h3>
-        <table class="grid-18">
-          <tr>
-            <th>OpenID</th>
-            <th></th>
-          </tr>
-          {% for oid in c.user.open_ids %}
-            {% set obj = c.user.openid_object(oid) %}
-          <tr>
-            <td>{{oid}}</td>
-            <td>{{lib.submit_button('Delete', 'oid-%s.delete' % loop.index0)}}</td>
-          </tr>
-          {% endfor %}
-        </table>
-        {% endif %}
-        <div class="grid-18">
-        <a href="/auth/claim_oid">Claim New OpenID</a>
-        </div>
-    {% endif %}
-    <div class="grid-18">
-    {{lib.submit_button('Save Changes')}}
-    </div>
-  </form>
-{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_prefs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_prefs.html b/Allura/allura/templates/user_prefs.html
new file mode 100644
index 0000000..0da9c9f
--- /dev/null
+++ b/Allura/allura/templates/user_prefs.html
@@ -0,0 +1,140 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+  <div class="grid-23">
+      <h2>Preferences</h2>
+      <form action="update" method="post">
+            {% if tg.config.get('auth.method', 'local') == 'local' %}
+            <label class="grid-4">Display Name</label>
+            <div class="grid-18">
+              <input name="preferences.display_name" value="{{c.user.display_name}}" type="text">
+            </div>
+            <label class="grid-4">Page Size</label>
+            <div class="grid-18">
+              <select name="preferences.results_per_page">
+                {% for per_page in [25, 50, 100, 250] %}
+                    <option {% if per_page == c.user.preferences.results_per_page %}selected="selected"{% endif %}
+                       value="{{per_page}}">{{per_page}}</option>
+                {% endfor %}
+              </select>
+            </div>
+            {% endif %}
+
+        {% if tg.config.get('auth.method', 'local') == 'local' %}
+          {% for a in c.user.email_addresses %}
+            <input name="addr-{{loop.index0}}.ord" value="{{loop.index0}}" type="hidden"/>
+          {% endfor %}
+          {% if c.user.email_addresses %}
+            <h3 class="grid-18">Email Addresses</h3>
+            <table class="grid-18">
+              <tr>
+                <th>Primary?</th>
+                <th>Address</th>
+                <th>Confirmed</th>
+                <th></th>
+              </tr>
+              {% for a in c.user.email_addresses %}
+              <tr>
+                {% set obj = c.user.address_object(a) %}
+                <td>{{lib.radio_button('primary_addr', None, a, c.user.preferences.email_address)}}</td>
+                <td>{{a}}</td>
+                {% if obj %}
+                <td>
+                  {% if obj.confirmed %}
+                    yes
+                  {% else %}
+                    no (<a href="{{g.url('/auth/send_verification_link', a=a)}}">verify</a>)
+                  {% endif %}
+                </td>
+                {% else %}
+                  <td>Unknown addr obj {{a}}</td>
+                {% endif %}
+                <td>{{lib.submit_button('Delete', 'addr-%s.delete' % loop.index0)}}</td>
+              </tr>
+              {% endfor %}
+            </table>
+            {% endif %}
+            <div class="grid-18">
+            {{lib.text_field('new_addr.addr', 'New Email Address')}}
+            {{lib.submit_button('Claim Address', name='new_addr.claim')}}
+            </div>
+
+            {% if c.user.open_ids %}
+            <h3 class="grid-18">OpenIDs Claimed</h3>
+            <table class="grid-18">
+              <tr>
+                <th>OpenID</th>
+                <th></th>
+              </tr>
+              {% for oid in c.user.open_ids %}
+                {% set obj = c.user.openid_object(oid) %}
+              <tr>
+                <td>{{oid}}</td>
+                <td>{{lib.submit_button('Delete', 'oid-%s.delete' % loop.index0)}}</td>
+              </tr>
+              {% endfor %}
+            </table>
+            {% endif %}
+            <div class="grid-18">
+            <a href="/auth/claim_oid">Claim New OpenID</a>
+            </div>
+        {% endif %}
+        <div class="grid-18">
+        {{lib.submit_button('Save Changes')}}
+        </div>
+      </form>
+  </div>
+
+   <div style="clear:both"></div>
+  {% if g.theme.password_change_form %}
+  <div class="grid-20">
+    <h2>Change Password</h2>
+    {{ g.theme.password_change_form.display() }}
+  </div>
+  {% endif %}
+
+  {% if g.theme.upload_key_form %}
+  <div class="grid-20">
+    <h2>Upload ssh public key</h2>
+    {{ g.theme.upload_key_form.display() }}
+  </div>
+  {% endif %}
+
+  {% if tg.config.get('auth.method', 'local') == 'local' %}
+  <div class="grid-20">
+      <h2>API Token</h2>
+      {% if api_token %}
+        <p>
+          <b>API Key:</b><br/>
+          {{api_token.api_key}}<br/>
+          <b>Secret Key:</b><br/>
+          {{api_token.secret_key}}<br/>
+        </p>
+        <form method="POST" action="del_api_token" class="grid-18">
+          <input type="submit" value="Delete API Token">
+        </form>
+      {% else %}
+        <p>No API token generated</p>
+      {% endif %}
+      <form method="POST" action="gen_api_token" class="grid-18">
+        <input type="submit" value="(Re)generate API Token">
+      </form>
+  </div>
+  {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/templates/user_skills.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_skills.html b/Allura/allura/templates/user_skills.html
index 667ff70..bf58d19 100644
--- a/Allura/allura/templates/user_skills.html
+++ b/Allura/allura/templates/user_skills.html
@@ -6,6 +6,17 @@
 {% block header %}Skills manager for {{c.user.username}} {% endblock %}
 
 {% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+  </ul>
+
   <div class="grid-20">
     {% if c.user.get_skills()|length > 0 %}
       <h2>Your current skills list:</h2>
@@ -43,9 +54,9 @@
           You selected:
         </div>
         <div class="grid-12" style="margin-bottom:20px">      
-           <a href="user_skills">List of all skills</a>
+           <a href=".">List of all skills</a>
            {% for cat in parents %}
-             &gt; <a href="/auth/prefs/user_skills/{{cat.shortname}}">{{cat.fullname}}</a>
+             &gt; <a href="{{cat.shortname}}">{{cat.fullname}}</a>
            {% endfor %}
            &gt; <b>{{selected_skill.fullname}}</b>
            <input type="hidden" name="upper_category" value="{{selected_skill.trove_parent_id}}"/>
@@ -64,23 +75,7 @@
     {% 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")}}
+           action=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/b55760a6/Allura/allura/templates/user_subs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_subs.html b/Allura/allura/templates/user_subs.html
new file mode 100644
index 0000000..457e7a7
--- /dev/null
+++ b/Allura/allura/templates/user_subs.html
@@ -0,0 +1,54 @@
+{% set hide_left_bar = True %}
+{% extends g.theme.master %}
+
+{% block title %}{{c.user.username}} / Preferences{% endblock %}
+
+{% block header %}User Preferences for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  <ul id="account-nav-menu" class="b-hornav droppy">
+      {% for item in menu -%}
+      <li id="{{ item.tabid }}">
+      <a href="{{ item.target }}">
+          {{ item.title }}
+          <div class="marker{% if item.target.rstrip('/') == request.path.rstrip('/') %} current{% endif %}"></div>
+      </a>
+      </li>
+      {%- endfor %}
+   </ul>
+
+   <h2>Authorized Third-party Applications</h2>
+   {% for access_tok in authorized_applications %}
+     <div>
+       <h3>{{access_tok.consumer_token.name}}</h3>
+       {{access_tok.consumer_token.description_html}}
+       {{ c.revoke_access.display(value=access_tok) }}
+       <br style="clear:both"/>
+   </div>
+  {% endfor %}
+     {% if not authorized_applications %}<p>No authorized third-party applications</p>{% endif %}
+
+  <h2>Subscriptions</h2>
+  {% if subscriptions %}
+    <p><em>Mark tools that you want to subscribe to. Unmark tools that you want to unsubscribe from. Press 'Save' button.</em></p>
+    {{c.form.display(action='update_subscriptions', value=dict(subscriptions=subscriptions))}}
+  {% else%}
+    <p>No subscriptions.</p>
+  {% endif %}
+  <hr/>
+  <div style="clear:both">&nbsp;</div>
+  <form action="update" method="post">
+        <label class="grid-4">Email Format</label>
+        <div class="grid-18">
+          <select name="preferences.email_format">
+            <option value="plain" {{'selected' if c.user.preferences.email_format == 'plain' else ''}}>Plain Text</option>
+            <option value="html" {{'selected' if c.user.preferences.email_format == 'html' else ''}}>HTML</option>
+            <option value="both" {{'selected' if c.user.preferences.email_format == 'both' else ''}}>Combined</option>
+          </select>
+        </div>
+
+    <div class="grid-18">
+    {{lib.submit_button('Save Changes')}}
+    </div>
+  </form>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b55760a6/Allura/allura/tests/functional/test_auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 0b4bb1c..666c3ec 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -41,19 +41,19 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs(self):
-        r = self.app.get('/auth/prefs/', extra_environ=dict(username='test-admin'))
+        r = self.app.get('/auth/preferences/', extra_environ=dict(username='test-admin'))
         assert 'test@example.com' not in r
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'new_addr.addr':'test@example.com',
                  'new_addr.claim':'Claim Address',
                  'primary_addr':'test-admin@users.localhost',
                  'preferences.email_format':'plain'},
                 extra_environ=dict(username='test-admin'))
-        r = self.app.get('/auth/prefs/')
+        r = self.app.get('/auth/preferences/')
         assert 'test@example.com' in r
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'addr-1.ord':'1',
                  'addr-2.ord':'1',
                  'addr-2.delete':'on',
@@ -61,13 +61,13 @@ class TestAuth(TestController):
                  'primary_addr':'test-admin@users.localhost',
                  'preferences.email_format':'plain'},
                 extra_environ=dict(username='test-admin'))
-        r = self.app.get('/auth/prefs/')
+        r = self.app.get('/auth/preferences/')
         assert 'test@example.com' not in r
         ea = M.EmailAddress.query.get(_id='test-admin@users.localhost')
         ea.confirmed = True
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/auth/prefs/update', params={
-                 'display_name':'Test Admin',
+        r = self.app.post('/auth/preferences/update', params={
+                 'preferences.display_name':'Test Admin',
                  'new_addr.addr':'test-admin@users.localhost',
                  'new_addr.claim':'Claim Address',
                  'primary_addr':'test-admin@users.localhost',
@@ -76,7 +76,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions(self):
-        r = self.app.get('/auth/prefs/',
+        r = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         subscriptions = M.Mailbox.query.find(dict(
             user_id=c.user._id, is_flash=False)).all()
@@ -118,7 +118,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_subscribe(self):
-        resp = self.app.get('/auth/prefs/',
+        resp = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         # find not subscribed tool, subscribe and verify
@@ -134,7 +134,7 @@ class TestAuth(TestController):
 
     @td.with_user_project('test-admin')
     def test_prefs_subscriptions_unsubscribe(self):
-        resp = self.app.get('/auth/prefs/',
+        resp = self.app.get('/auth/subscriptions/',
                 extra_environ=dict(username='test-admin'))
         form = self._find_subscriptions_form(resp)
         field_name = self._find_subscriptions_field(form, subscribed=True)
@@ -147,15 +147,15 @@ class TestAuth(TestController):
         assert not s, "User still has subscription with Mailbox._id %s" % s_id
 
     def test_api_key(self):
-         r = self.app.get('/auth/prefs/')
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' in r
-         r = self.app.post('/auth/prefs/gen_api_token', status=302)
-         r = self.app.get('/auth/prefs/')
+         r = self.app.post('/auth/preferences/gen_api_token', status=302)
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' not in r
          assert 'API Key:' in r
          assert 'Secret Key:' in r
-         r = self.app.post('/auth/prefs/del_api_token', status=302)
-         r = self.app.get('/auth/prefs/')
+         r = self.app.post('/auth/preferences/del_api_token', status=302)
+         r = self.app.get('/auth/preferences/')
          assert 'No API token generated' in r
 
     def test_oauth(self):
@@ -382,10 +382,10 @@ class TestPreferences(TestController):
         from pytz import country_names
         setsex, setbirthdate, setcountry, setcity, settimezone = \
             ('Male', '19/08/1988', 'IT', 'Milan', 'Europe/Rome')
-        result = self.app.get('/auth/prefs')
+        result = self.app.get('/auth/user_info')
 
         #Check if personal data is properly set
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(
                  sex=setsex,
                  birthdate=setbirthdate,
@@ -405,7 +405,7 @@ class TestPreferences(TestController):
         assert timezone == settimezone
 
         #Check if setting a wrong date everything works correctly
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(birthdate='30/02/1998'))
         assert 'Please enter a valid date' in str(r)
         user = M.User.query.get(username='test-admin')
@@ -421,7 +421,7 @@ class TestPreferences(TestController):
         assert timezone == settimezone
 
         #Check deleting birthdate
-        r = self.app.post('/auth/prefs/change_personal_data',
+        r = self.app.post('/auth/user_info/change_personal_data',
              params=dict(
                  sex=setsex,
                  birthdate='',
@@ -435,8 +435,8 @@ class TestPreferences(TestController):
     def test_contacts(self):
         #Add skype account
         testvalue = 'testaccount'
-        result = self.app.get('/auth/prefs/user_contacts')
-        r = self.app.post('/auth/prefs/user_contacts/skype_account',
+        result = self.app.get('/auth/user_info/contacts')
+        r = self.app.post('/auth/user_info/contacts/skype_account',
              params=dict(skypeaccount=testvalue))
         user = M.User.query.get(username='test-admin')
         assert user.skypeaccount == testvalue
@@ -444,7 +444,7 @@ class TestPreferences(TestController):
         #Add social network account
         socialnetwork = 'Facebook'
         accounturl = 'http://www.facebook.com/test'
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork,
                          accounturl = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -455,7 +455,7 @@ class TestPreferences(TestController):
         #Add second social network account
         socialnetwork2 = 'Twitter'
         accounturl2 = 'http://twitter.com/test'
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(socialnetwork=socialnetwork2,
                          accounturl = '@test'))
         user = M.User.query.get(username='test-admin')
@@ -464,7 +464,7 @@ class TestPreferences(TestController):
                 {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks)
 
         #Remove first social network account
-        r = self.app.post('/auth/prefs/user_contacts/remove_social_network',
+        r = self.app.post('/auth/preferences/contacts/remove_social_network',
              params=dict(socialnetwork=socialnetwork,
                          account = accounturl))
         user = M.User.query.get(username='test-admin')
@@ -472,7 +472,7 @@ class TestPreferences(TestController):
                {'socialnetwork':socialnetwork2, 'accounturl':accounturl2} in user.socialnetworks
 
         #Add invalid social network account
-        r = self.app.post('/auth/prefs/user_contacts/add_social_network',
+        r = self.app.post('/auth/preferences/contacts/add_social_network',
              params=dict(accounturl = accounturl, socialnetwork=''))
         user = M.User.query.get(username='test-admin')
         assert len(user.socialnetworks) == 1 and \
@@ -480,40 +480,40 @@ class TestPreferences(TestController):
 
         #Add telephone number
         telnumber = '+3902123456'
-        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
+        r = self.app.post('/auth/preferences/contacts/add_telnumber',
              params=dict(newnumber=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and (user.telnumbers[0] == telnumber))
 
         #Add second telephone number
         telnumber2 = '+3902654321'
-        r = self.app.post('/auth/prefs/user_contacts/add_telnumber',
+        r = self.app.post('/auth/preferences/contacts/add_telnumber',
              params=dict(newnumber=telnumber2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 2 and telnumber in user.telnumbers and telnumber2 in user.telnumbers)
 
         #Remove first telephone number
-        r = self.app.post('/auth/prefs/user_contacts/remove_telnumber',
+        r = self.app.post('/auth/preferences/contacts/remove_telnumber',
              params=dict(oldvalue=telnumber))
         user = M.User.query.get(username='test-admin')
         assert (len(user.telnumbers) == 1 and telnumber2 in user.telnumbers)
 
         #Add website
         website = 'http://www.testurl.com'
-        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
+        r = self.app.post('/auth/preferences/contacts/add_webpage',
              params=dict(newwebsite=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and (website in user.webpages))
 
         #Add second website
         website2 = 'http://www.testurl2.com'
-        r = self.app.post('/auth/prefs/user_contacts/add_webpage',
+        r = self.app.post('/auth/preferences/contacts/add_webpage',
              params=dict(newwebsite=website2))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 2 and website in user.webpages and website2 in user.webpages)
 
         #Remove first website
-        r = self.app.post('/auth/prefs/user_contacts/remove_webpage',
+        r = self.app.post('/auth/preferences/contacts/remove_webpage',
              params=dict(oldvalue=website))
         user = M.User.query.get(username='test-admin')
         assert (len(user.webpages) == 1 and website2 in user.webpages)
@@ -527,8 +527,8 @@ class TestPreferences(TestController):
         starttime = time(9,0,0)
         endtime = time(12, 0, 0)
 
-        result = self.app.get('/auth/prefs/user_availability')
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        result = self.app.get('/auth/preferences/availability')
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -542,7 +542,7 @@ class TestPreferences(TestController):
         endtime2 = time(16, 0, 0)
 
         #Add second availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=starttime2.strftime('%H:%M'),
@@ -553,7 +553,7 @@ class TestPreferences(TestController):
                and timeslot2dict in user.get_availability_timeslots()
 
         #Remove availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/remove_timeslot',
+        r = self.app.post('/auth/preferences/availability/remove_timeslot',
              params=dict(
                  weekday=weekday,
                  starttime=starttime.strftime('%H:%M'),
@@ -562,7 +562,7 @@ class TestPreferences(TestController):
         assert len(user.availability) == 1 and timeslot2dict in user.get_availability_timeslots()
 
         #Add invalid availability timeslot
-        r = self.app.post('/auth/prefs/user_availability/add_timeslot',
+        r = self.app.post('/auth/preferences/availability/add_timeslot',
              params=dict(
                  weekday=weekday2,
                  starttime=endtime2.strftime('%H:%M'),
@@ -581,8 +581,8 @@ class TestPreferences(TestController):
         now = datetime(now.year, now.month, now.day)
         startdate = now + timedelta(days=1)
         enddate = now + timedelta(days=7)
-        result = self.app.get('/auth/prefs/user_availability')
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        result = self.app.get('/auth/preferences/availability')
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -593,7 +593,7 @@ class TestPreferences(TestController):
         #Add second inactivity period
         startdate2 =  now + timedelta(days=24)
         enddate2 = now + timedelta(days=28)
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate=startdate2.strftime('%d/%m/%Y'),
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -603,7 +603,7 @@ class TestPreferences(TestController):
                and period2dict in user.get_inactive_periods()
 
         #Remove first inactivity period
-        r = self.app.post('/auth/prefs/user_availability/remove_inactive_period',
+        r = self.app.post('/auth/preferences/availability/remove_inactive_period',
              params=dict(
                  startdate=startdate.strftime('%d/%m/%Y'),
                  enddate=enddate.strftime('%d/%m/%Y')))
@@ -611,7 +611,7 @@ class TestPreferences(TestController):
         assert len(user.inactiveperiod) == 1 and period2dict in user.get_inactive_periods()
 
         #Add invalid inactivity period
-        r = self.app.post('/auth/prefs/user_availability/add_inactive_period',
+        r = self.app.post('/auth/preferences/availability/add_inactive_period',
              params=dict(
                  startdate='NOT/A/DATE',
                  enddate=enddate2.strftime('%d/%m/%Y')))
@@ -627,8 +627,8 @@ class TestPreferences(TestController):
         skill_cat = M.TroveCategory.query.get(show_as_skill=True)
         level = 'low'
         comment = 'test comment'
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -640,8 +640,8 @@ class TestPreferences(TestController):
         #Add again the same skill
         level = 'medium'
         comment = 'test comment 2'
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level,
                  comment=comment,
@@ -653,7 +653,7 @@ class TestPreferences(TestController):
         #Add an invalid skill
         level2 = 'not a level'
         comment2 = 'test comment 2'
-        r = self.app.post('/auth/prefs/user_skills/save_skill',
+        r = self.app.post('/auth/preferences/skills/save_skill',
              params=dict(
                  level=level2,
                  comment=comment2,
@@ -663,8 +663,8 @@ class TestPreferences(TestController):
         assert len(user.skills) == 1 and skilldict in user.skills
 
         #Remove a skill
-        result = self.app.get('/auth/prefs/user_skills')
-        r = self.app.post('/auth/prefs/user_skills/remove_skill',
+        result = self.app.get('/auth/preferences/skills')
+        r = self.app.post('/auth/preferences/skills/remove_skill',
              params=dict(
                  categoryid=str(skill_cat.trove_cat_id)))
         user = M.User.query.get(username='test-admin')