blob: 9be92763e36d80e76cf9ee120e64d586ec60d75d [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.
import logging
import re
import pkg_resources
from pylons import tmpl_context as c
from pylons import app_globals as g
from pylons import request
from formencode import validators
from tg import expose, redirect, validate, flash
import tg
from webob import exc
from jinja2 import Markup
from pytz import timezone
from datetime import datetime
from paste.deploy.converters import asbool
from allura import version
from allura.app import Application, SitemapEntry
from allura.lib import helpers as h
from allura.lib.security import require_access
from allura.lib.plugin import AuthenticationProvider
from allura.model import User, ACE, ProjectRole
from allura.controllers import BaseController
from allura.controllers.feed import FeedArgs, FeedController
from allura.controllers.rest import AppRestControllerMixin
from allura.lib.decorators import require_post
from allura.lib.widgets.user_profile import SendMessageForm
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
sections = {}
for ep in h.iter_entry_points('allura.user_profile.sections'):
sections[ep.name] = ep.load()
section_ordering = tg.config.get('user_profile_sections.order', '')
ordered_sections = []
for section in re.split(r'\s*,\s*', section_ordering):
if section in sections:
ordered_sections.append(sections.pop(section))
UserProfileApp._sections = ordered_sections + sections.values()
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(request.referer)
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(request.referer)
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(request.referer)
if to_user.get_pref('disable_user_messages'):
flash('This user has disabled direct email messages', 'info')
redirect(request.referer)
@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]
return dict(
user=user,
reg_date=provider.user_registration_date(user),
sections=sections)
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
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):
"""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():
c.user.send_user_message(
c.project.user_project_of, subject, message, cc)
flash("Message sent.")
else:
flash("You can't send more than %i messages per %i seconds" % (
c.user.user_message_max_messages,
c.user.user_message_time_interval), 'error')
return redirect(c.project.user_project_of.url())
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(object):
"""
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]``.
"""
template = ''
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.
"""
self.user = user
self.project = project
def check_display(self):
"""
Should return True if the section should be displayed.
"""
return True
def prepare_context(self, context):
"""
Should be overridden to add any values to the template context prior
to display.
"""
return context
def display(self, *a, **kw):
"""
Renders the section using the context from :meth:`prepare_context`
and the :attr:`template`, if :meth:`check_display` returns True.
If overridden or this base class is not used, this method should
return either plain text (which will be escaped) or a `jinja2.Markup`
instance.
"""
if not self.check_display():
return ''
try:
tmpl = g.jinja2_env.get_template(self.template)
context = self.prepare_context({
'h': h,
'c': c,
'g': g,
'user': self.user,
'config': tg.config,
'auth': AuthenticationProvider.get(request),
})
return Markup(tmpl.render(context))
except Exception as e:
log.exception('Error rendering profile section %s: %s', type(self).__name__, e)
if asbool(tg.config.get('debug')):
raise
else:
return ''
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):
template = 'allura.ext.user_profile:templates/sections/projects.html'
def get_projects(self):
return [
project
for project in self.user.my_projects()
if project != c.project
and (self.user == c.user or h.has_access(project, 'read'))
and not project.is_nbhd_project
and not project.is_user_project]
def prepare_context(self, context):
context['projects'] = self.get_projects()
return context
def __json__(self):
projects = [
dict(
name=project['name'],
url=project.url(),
summary=project['summary'],
last_updated=project['last_updated'])
for project in self.get_projects()]
return dict(projects=projects)
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())