blob: 22b4200738b80b999f5f0ccbfb706dfdb5d48d31 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
import pkg_resources
from datetime import datetime
import six
from formencode import validators
from tg import request
from tg import tmpl_context as c, app_globals as g
from pytz import timezone
from tg import expose, redirect, validate, flash
from tg.decorators import without_trailing_slash
from decorator import decorator
from webob import exc
from allura import version
from allura.app import Application, SitemapEntry
from allura.controllers import BaseController
from allura.controllers.feed import FeedArgs, FeedController
from allura.controllers.rest import AppRestControllerMixin
from allura.lib import helpers as h
from allura.lib.decorators import require_post
from allura.lib.plugin import AuthenticationProvider
from allura.lib.security import require_access
from allura.lib.widgets.user_profile import SendMessageForm, SectionsUtil, SectionBase, ProjectsSectionBase
from allura.model import User, ACE, ProjectRole
log = logging.getLogger(__name__)
class F(object):
send_message = SendMessageForm()
class UserProfileApp(Application):
"""
This is the Profile tool, which is automatically installed as
the default (first) tool on any user project.
"""
__version__ = version.__version__
tool_label = 'Profile'
max_instances = 0
has_notifications = False
icons = {
24: 'images/home_24.png',
32: 'images/home_32.png',
48: 'images/home_48.png'
}
def __init__(self, user, config):
Application.__init__(self, user, config)
self.root = UserProfileController()
self.templates = pkg_resources.resource_filename(
'allura.ext.user_profile', 'templates')
self.api_root = UserProfileRestController()
@property
@h.exceptionless([], log)
def sitemap(self):
return [SitemapEntry('Profile', '.')]
def admin_menu(self):
return []
def main_menu(self):
return [SitemapEntry('Profile', '.')]
def is_visible_to(self, user):
# we don't work with user subprojects
return c.project.is_root
def install(self, project):
pr = ProjectRole.by_user(c.user)
if pr:
self.config.acl = [
ACE.allow(pr._id, perm)
for perm in self.permissions]
def uninstall(self, project): # pragma no cover
pass
@property
def profile_sections(self):
"""
Loads and caches user profile sections from the entry-point
group ``[allura.user_profile.sections]``.
Profile sections are loaded unless disabled (see
`allura.lib.helpers.iter_entry_points`) and are sorted according
to the `user_profile_sections.order` config value.
The config should contain a comma-separated list of entry point names
in the order that they should appear in the profile. Unknown or
disabled sections are ignored, and available sections that are not
specified in the order come after all explicitly ordered sections,
sorted randomly.
"""
if hasattr(UserProfileApp, '_sections'):
return UserProfileApp._sections
UserProfileApp._sections = SectionsUtil.load_sections('user_profile')
return UserProfileApp._sections
class UserProfileController(BaseController, FeedController):
def _check_security(self):
require_access(c.project, 'read')
def _check_can_message(self, from_user, to_user):
if from_user is User.anonymous():
flash('You must be logged in to send user messages.', 'info')
redirect(six.ensure_text(request.referer or '/'))
if not (from_user and from_user.get_pref('email_address')):
flash('In order to send messages, you must have an email address '
'associated with your account.', 'info')
redirect(six.ensure_text(request.referer or '/'))
if not (to_user and to_user.get_pref('email_address')):
flash('This user can not receive messages because they do not have '
'an email address associated with their account.', 'info')
redirect(six.ensure_text(request.referer or '/'))
if to_user.get_pref('disable_user_messages'):
flash('This user has disabled direct email messages', 'info')
redirect(six.ensure_text(request.referer or '/'))
@expose('jinja:allura.ext.user_profile:templates/user_index.html')
def index(self, **kw):
user = c.project.user_project_of
if not user:
raise exc.HTTPNotFound()
provider = AuthenticationProvider.get(request)
sections = [section(user, c.project)
for section in c.app.profile_sections]
noindex = True
for s in sections:
s.setup_context()
if s.context.get('projects') or s.context.get('timeline'):
noindex = False
return dict(
user=user,
reg_date=provider.user_registration_date(user),
sections=sections,
noindex=noindex,
)
def get_feed(self, project, app, user):
"""Return a :class:`allura.controllers.feed.FeedArgs` object describing
the xml feed for this controller.
Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
"""
user = project.user_project_of
return FeedArgs(
{'author_link': user.url()},
'Recent posts by %s' % user.display_name,
project.url())
@expose('jinja:allura.ext.user_profile:templates/send_message.html')
def send_message(self):
"""Render form for sending a message to another user.
"""
self._check_can_message(c.user, c.project.user_project_of)
delay = c.user.time_to_next_user_message()
expire_time = str(delay) if delay else None
c.form = F.send_message
if c.user.get_pref('message_reply_real_address'):
c.form.fields.reply_to_real_address.attrs = {'checked': 'checked'}
return dict(user=c.project.user_project_of, expire_time=expire_time)
@require_post()
@expose()
@validate(dict(subject=validators.NotEmpty,
message=validators.NotEmpty))
def send_user_message(self, subject='', message='', cc=None, reply_to_real_address=None):
"""Handle POST for sending a message to another user.
"""
self._check_can_message(c.user, c.project.user_project_of)
if cc:
cc = c.user.get_pref('email_address')
if c.user.can_send_user_message():
if reply_to_real_address:
c.user.set_pref('message_reply_real_address', True)
else:
c.user.set_pref('message_reply_real_address', False)
c.user.send_user_message(
c.project.user_project_of, subject, message, cc, reply_to_real_address, c.user.preferences.email_address)
flash("Message sent.")
else:
flash("You can't send more than %i messages per %i seconds" % (
g.user_message_max_messages,
g.user_message_time_interval), 'error')
return redirect(c.project.user_project_of.url())
@without_trailing_slash
@expose('jinja:allura.ext.user_profile:templates/user_card.html')
def user_card(self):
u = c.project.user_project_of
locationData = u.get_pref('localization')
webpages = u.get_pref('webpages')
location = ''
website = ''
if locationData.city and locationData.country:
location = locationData.city + ', ' + locationData.country
elif locationData.country and not locationData.city:
location = locationData.country
elif locationData.city and not locationData.country:
location = locationData.city
if len(webpages) > 0:
website = webpages[0]
return dict(
user=u,
location=location,
website=website)
class UserProfileRestController(AppRestControllerMixin):
@expose('json:')
def index(self, **kw):
user = c.project.user_project_of
if not user:
raise exc.HTTPNotFound()
sections = [section(user, c.project)
for section in c.app.profile_sections]
json = {}
for s in sections:
if hasattr(s, '__json__'):
json.update(s.__json__())
return json
class ProfileSectionBase(SectionBase):
"""
This is the base class for sections on the Profile tool.
.. py:attribute:: template
A resource string pointing to the template for this section. E.g.::
template = "allura.ext.user_profile:templates/projects.html"
Sections must be pointed to by an entry-point in the group
``[allura.user_profile.sections]``.
"""
def __init__(self, user, project):
"""
Creates a section for the given :param:`user` and user
:param:`project`. Stores the values as attributes of
the same name.
"""
super(ProfileSectionBase, self).__init__(user)
self.project = project
class PersonalDataSection(ProfileSectionBase):
template = 'allura.ext.user_profile:templates/sections/personal-data.html'
def prepare_context(self, context):
context['timezone'] = self.user.get_pref('timezone')
if context['timezone']:
tz = timezone(context['timezone'])
context['timezone'] = tz.tzname(datetime.utcnow())
return context
def __json__(self):
auth_provider = AuthenticationProvider.get(request)
return dict(
username=self.user.username,
name=self.user.display_name,
joined=auth_provider.user_registration_date(self.user),
localization=self.user.get_pref('localization')._deinstrument(),
sex=self.user.get_pref('sex'),
telnumbers=self.user.get_pref('telnumbers')._deinstrument(),
skypeaccount=self.user.get_pref('skypeaccount'),
webpages=self.user.get_pref('webpages')._deinstrument(),
availability=self.user.get_pref('availability')._deinstrument())
class ProjectsSection(ProfileSectionBase, ProjectsSectionBase):
template = 'allura.ext.user_profile:templates/sections/projects.html'
class SkillsSection(ProfileSectionBase):
template = 'allura.ext.user_profile:templates/sections/skills.html'
def __json__(self):
return dict(skills=self.user.get_skills())
class ToolsSection(ProfileSectionBase):
template = 'allura.ext.user_profile:templates/sections/tools.html'
class SocialSection(ProfileSectionBase):
template = 'allura.ext.user_profile:templates/sections/social.html'
def __json__(self):
return dict(
socialnetworks=self.user.get_pref('socialnetworks')._deinstrument())