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"> </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 = " "
+
+ 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 %}
+ > <a href="/categories/{{cat.shortname}}">{{cat.fullname}}</a>
+ {% endfor %}
+ > {{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 %}
+ > <a href="/auth/prefs/user_skills/{{cat.shortname}}">{{cat.fullname}}</a>
+ {% endfor %}
+ > <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"> </label>
- <div class="grid-{{15 + extra_width}}">
- {% for b in buttons %}
- {{b.display()}}
- {% endfor %}
- </div>
+ {% if buttons %}
+ <label class="grid-4"> </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>