# -*- coding: utf-8 -*-
from dexterity.membrane.behavior.password import IProvidePasswordsSchema
from itertools import imap
from plone import api as plone_api
from plone.api.exc import InvalidParameterError
from ploneintranet.network.graph import decode
from ploneintranet.network.interfaces import INetworkTool
from ploneintranet.userprofile.content.userprofile import IUserProfile
from ploneintranet.userprofile.interfaces import IMemberGroup
from Products.CMFPlone.utils import safe_unicode
from z3c.form.interfaces import IValidator
from zope.component import getMultiAdapter
from zope.component import queryUtility
import random
import string
[docs]def get_users(context=None, full_objects=True, **kwargs):
"""
List users from catalog, avoiding expensive LDAP lookups.
:param context: Any content object that will be used to find the
UserResolver context
:type context: Content object
:param full_objects: A switch to indicate if full objects or brains should
be returned
:type full_objects: boolean
:returns: user brains or user objects
:rtype: iterator
"""
try:
mtool = plone_api.portal.get_tool("membrane_tool")
except InvalidParameterError:
return []
if context:
acl_users = plone_api.portal.get_tool("acl_users")
try:
# adapters provided by pi.userprofile and pi.workspace
members = set([x for x in IMemberGroup(context).members])
# In case of groups, resolve the group members
for id in list(members):
group = acl_users.getGroupById(id)
if group:
members.remove(id)
# these are not membrane profiles but acl members
members = members.union(
set([user.getId() for user in group.getGroupMembers()])
)
# both context and query: calculate intersection
if "exact_getUserId" in kwargs:
_combi = list(members.intersection(set(kwargs["exact_getUserId"])))
kwargs["exact_getUserId"] = _combi
else:
kwargs["exact_getUserId"] = list(members)
except TypeError:
# could not adapt to IMemberGroup
pass
portal_type = ("ploneintranet.userprofile.userprofile",)
search_results = mtool.searchResults(portal_type=portal_type, **kwargs)
if full_objects:
return (x.getObject() for x in search_results)
else:
return search_results
[docs]def get_userids():
"""
For the moment it just returns all the ids of the userprofiles
we have in the site.
:returns: the userprofile ids
:rtype: iterator
"""
portal = plone_api.portal.get()
profiles = portal.get("profiles", {})
return profiles.keys()
[docs]def get_user_suggestions(context=None, full_objects=True, min_matches=5, **kwargs):
"""
This is a wrapper around get_users with the intent of providing
staggered suggestion of users for a user picker:
1. Users from the current context (workspace)
If not enough users, add:
2. Users followed by the current logged-in user
If not enough combined users from 1+2, fallback to:
3. All users in the portal.
List users from catalog, avoiding expensive LDAP lookups.
:param context: Any content object that will be used to find the
UserResolver context
:type context: Content object
:param full_objects: A switch to indicate if full objects or brains should
be returned
:type full_objects: boolean
:param min_matches: Keeps expanding search until this treshold is reached
:type min_matches: int
:returns: user brains or user objects
:rtype: iterator
"""
def expand(search_results, full_objects, **kwargs):
"""Helper function to delay full object expansion"""
# Filter results by chosen review state
if "review_state" in kwargs:
search_results = filter(
lambda x: getattr(x, "review_state", "")
== kwargs["review_state"], # noqa
search_results,
)
if full_objects:
return (x.getObject() for x in search_results)
else:
return search_results
# By default, only return users that are enabled
if "review_state" not in kwargs:
kwargs["review_state"] = "enabled"
# stage 1 context users
if context:
context_users = [x for x in get_users(context, False, **kwargs)]
if len(context_users) >= min_matches:
return expand(context_users, full_objects, **kwargs)
# prepare stage 2 and 3
all_users = [x for x in get_users(None, False, **kwargs)]
# skip stage 2 if not enough users
if len(all_users) < min_matches:
return expand(all_users, full_objects, **kwargs)
# prepare stage 2 filter - unicode!
graph = queryUtility(INetworkTool)
following_ids = [
x for x in graph.get_following("user", plone_api.user.get_current().id)
]
following_users = [x for x in all_users if decode(x.getUserId) in following_ids]
# apply stage 2 filter
if context:
filtered_users = set(context_users).union(set(following_users))
else:
filtered_users = following_users
if len(filtered_users) >= min_matches:
return expand(filtered_users, full_objects, **kwargs)
# fallback to stage 3 all users
return expand(all_users, full_objects, **kwargs)
[docs]def get_users_from_userids_and_groupids(ids=None):
"""
Given a list of userids and groupids return the set of users
FIXME this has to be folded into get_users
"""
acl_users = plone_api.portal.get_tool("acl_users")
userids = set([])
portal = plone_api.portal.get()
groups_container = portal.get("groups", {})
# BBB userprofile and workprofile should be in the same module
# to avoid circular imports
if groups_container:
mapping = {
group.getGroupId(): key for key, group in groups_container.objectItems()
}
else:
mapping = {}
for principalid in ids:
if principalid in mapping:
group = groups_container[mapping[principalid]]
else:
group = acl_users.getGroupById(principalid)
if group:
userids.update(group.getGroupMembers())
else:
userids.add(principalid)
return [user for user in imap(get, userids) if user]
[docs]def get(userid):
"""Get a Plone Intranet user profile by userid.
userid == username, but username != getUsername(), see #1043.
:param userid: Usernid of the user profile to be found
:type userid: string
:returns: User profile matching the given userid
:rtype: `ploneintranet.userprofile.content.userprofile.UserProfile` object
"""
# try first of all to get the user from the profiles folder
portal = plone_api.portal.get()
user = portal.unrestrictedTraverse("profiles/{}".format(userid), None)
if user is not None:
return user
# If we can't find the user there let's ask the membrane catalog
# and return the first result
for profile in get_users(exact_getUserId=userid):
return profile
# If we cannot find any match we will give up and return None
return None
[docs]def get_current():
"""Get the Plone Intranet user profile
for the current logged-in user
:returns: User profile matching the current logged-in user
:rtype: `ploneintranet.userprofile.content.userprofile.UserProfile` object
"""
if plone_api.user.is_anonymous():
return None
current_member = plone_api.user.get_current()
# non-membrane users (e.g. admin) have getUserName() but not getUserId()
userid = current_member.getId()
return get(userid)
[docs]def create(username, email=None, password=None, approve=False, properties=None):
"""Create a Plone Intranet user profile.
:param username: [required] The userid for the new user. WTF? see #1043.
:type username: string
:param email: [required] Email for the new user.
:type email: string
:param password: Password for the new user. If it's not set we generate
a random 12-char alpha-numeric one.
:type password: string
:param approve: If True, the user profile will be automatically approved
and be able to log in.
:type approve: boolean
:param properties: User properties to assign to the new user.
:type properties: dict
:returns: Newly created user
:rtype: `ploneintranet.userprofile.content.userprofile.UserProfile` object
"""
portal = plone_api.portal.get()
# We have to manually validate the username
validator = getMultiAdapter(
(portal, None, None, IUserProfile["username"], None), IValidator
)
validator.validate(safe_unicode(username))
# Generate a random password
if not password:
chars = string.ascii_letters + string.digits
password = "".join(random.choice(chars) for x in range(12))
profile_container = portal.contentValues(
{"portal_type": "ploneintranet.userprofile.userprofilecontainer"}
)[0]
if properties is None:
# Avoids using dict as default for a keyword argument.
properties = {}
if "fullname" in properties:
# Translate from plone-style 'fullname'
# to first and last names
fullname = properties.pop("fullname")
if " " in fullname:
firstname, lastname = fullname.split(" ", 1)
else:
firstname = ""
lastname = fullname
properties["first_name"] = firstname
properties["last_name"] = lastname
profile = plone_api.content.create(
container=profile_container,
type="ploneintranet.userprofile.userprofile",
id=username,
username=username,
email=email,
**properties
)
# We need to manually set the password via the behaviour
IProvidePasswordsSchema(profile).password = password
if approve:
plone_api.content.transition(profile, "approve")
profile.reindexObject()
return profile
[docs]def avatar_url(username=None):
"""Get the avatar image url for a user profile
:param username: Username for which to get the avatar url
:type username: string
:returns: absolute url for the avatar image
:rtype: string
"""
portal = plone_api.portal.get()
return "{0}/@@avatars/{1}".format(portal.absolute_url(), username)
[docs]def avatar_tag(username=None, link_to=None):
"""Get the tag that renders the user avatar wrapped in a link
:param username: Username for which to get the avatar url
:type username: string
:returns: HTML for the avatar tag
:rtype: string
"""
profile = get(username)
if not profile:
return ""
target_url = ""
profile_url = profile.absolute_url()
link_class = ["pat-avatar", "avatar"]
outer_tag = "a"
if link_to == "image":
if profile.portrait:
target_url = profile_url + "/@@avatar_profile.jpg"
link_class.extend(["pat-gallery", "user-info-avatar"])
else:
target_url = ""
link_class.append("user-info-avatar")
elif link_to == "profile":
target_url = profile_url
elif link_to is None:
outer_tag = "span"
img_class = []
if not profile.portrait:
img_class.append("default-user")
if target_url:
target_url = 'href="' + target_url + '"'
avatar_data = {
"outer_tag": outer_tag,
"fullname": profile.fullname,
"profile_url": profile_url,
"target_url": target_url,
"initials": profile.initials,
"title": profile.fullname or profile.getId() or username,
"link_class": " ".join(link_class),
"img_class": " ".join(img_class),
}
tag = u""" <{outer_tag} {target_url}
class="{link_class}"
data-initials="{initials}"
title="{title}"
>
<img src="{profile_url}/@@avatar_profile.jpg"
alt="Image of {fullname}"
class="{img_class}"
i18n:attributes="alt">
</{outer_tag}>""".format(
**avatar_data
)
return tag