PageRenderTime 2517ms CodeModel.GetById 30ms RepoModel.GetById 2ms app.codeStats 0ms

/source/members/member_basics.rst

https://github.com/dayontwari/collective.developermanual
ReStructuredText | 607 lines | 418 code | 189 blank | 0 comment | 0 complexity | 1e7a1d5372d29d74fb26b2b06125004b MD5 | raw file
  1. =============================
  2. Member manipulation
  3. =============================
  4. .. admonition:: Description
  5. How to programmatically create, read, edit and delete site members.
  6. .. contents:: :local:
  7. Introduction
  8. ============
  9. In Plone, there are two loosely-coupled subsystems relating to members:
  10. *Authentication and permission* information
  11. (``acl_users`` under site root), managed by the :term:`PAS`.
  12. In a default installation, this corresponds to Zope user objects.
  13. PAS is *pluggable*, though, so it may also be authenticating against
  14. an LDAP server, Plone content objects, or other sources.
  15. *Member profile* information
  16. accessible through the ``portal_membership`` tool.
  17. These represent Plone members. PAS authenticates,
  18. and the Plone member object provides metadata about the member.
  19. Getting the logged-in member
  20. ============================
  21. Anonymous and logged-in members are exposed via the
  22. :doc:`IPortalState context helper </misc/context>`.
  23. Example (browserview: use ``self.context`` since ``self`` is not
  24. acquisition-wrapped)::
  25. from zope.component import getMultiAdapter
  26. portal_state = getMultiAdapter(
  27. (self.context, self.request), name="plone_portal_state")
  28. if portal_state.anonymous():
  29. # Return target URL for the site anonymous visitors
  30. return self.product.getHomepageLink()
  31. else:
  32. # Return edit URL for the site members
  33. return product.absolute_url()
  34. or from a template:
  35. .. code-block:: html
  36. <div tal:define="username context/portal_membership/getAuthenticatedMember/getUserName">
  37. ...
  38. </div>
  39. Getting any member
  40. ==================
  41. To get a member by username (you must have ``Manager`` role)::
  42. mt = getToolByName(self.context, 'portal_membership')
  43. member = mt.getMemberById(username)
  44. To get all usernames::
  45. mt = getToolByName(self.context, 'portal_membership')
  46. memberIds = mt.listMemberIds()
  47. Getting member information
  48. ==========================
  49. Once you have access to the member object,
  50. you can grab basic information about it.
  51. Get the user's name::
  52. member.getName()
  53. Reseting user password without emailing them
  54. -----------------------------------------------
  55. * https://plone.org/documentation/kb/reset-a-password-without-having-to-email-one-to-the-user
  56. Exporting and importing member passwords
  57. ----------------------------------------
  58. You can also get at the hash of the user's password
  59. (only the hash is available, and only for standard Plone user objects)
  60. (in this example we're in Plone add-on context, since ``self`` is
  61. acquisition-wrapped)::
  62. uf = getToolByName(self, 'acl_users')
  63. passwordhash_map = uf.source_users._user_passwords
  64. userpasswordhash = passwordhash_map.get(member.id, '')
  65. Note that this is a private data structure.
  66. Depending on the Plone version and add-ons in use, it may not be available.
  67. You can use this hash directly when importing your user data,
  68. for example as follows (can be executed from a
  69. :doc:`debug prompt </misc/commandline>`.)::
  70. # The file 'exported.txt' contains lines with: "memberid hash"
  71. lines = open('exported.txt').readlines()
  72. changes = []
  73. c = 0
  74. members = mt.listMembers()
  75. for l in lines:
  76. memberid, passwordhash_exported = l.split(' ')
  77. passwordhash_exported = passwordhash_exported.strip()
  78. member = mt.getMemberById(memberid)
  79. if not member:
  80. print 'missing', memberid
  81. continue
  82. passwordhash = passwordhash_map.get(memberid)
  83. if passwordhash != passwordhash_exported:
  84. print 'changed', memberid, passwordhash, passwordhash_exported
  85. c += 1
  86. changes.append((memberid, passwordhash_exported))
  87. uf.source_users._user_passwords.update(changes)
  88. Also, take a look at a script for exporting Plone 3.0's memberdata and
  89. passwords:
  90. * http://blog.kagesenshi.org/2008/05/exporting-plone30-memberdata-and.html
  91. Iterating all site users
  92. ============================
  93. Example::
  94. buffer = ""
  95. # Returns list of site usernames
  96. mt = getToolByName(self, 'portal_membership')
  97. users = mt.listMemberIds()
  98. # alternative: get member objects
  99. # members = mt.listMembers()
  100. for user in users:
  101. print "Got username:" + user
  102. .. note::
  103. Zope users, such as *admin*, are not included in this list.
  104. Getting all *Members* for a given *Role*
  105. ========================================
  106. In this example we use the ``portal_membership`` tool.
  107. We assume that a role called ``Agent`` exists and that we already
  108. have the context::
  109. from Products.CMFCore.utils import getToolByName
  110. membership_tool = getToolByName(self, 'portal_membership')
  111. agents = [member for member in membership_tool.listMembers()
  112. if member.has_role('Agent')]
  113. Groups
  114. ======
  115. Groups are stored as ``PloneGroup`` objects. ``PloneGroup`` is a subclass of
  116. ``PloneUser``. Groups are managed by the ``portal_groups`` tool.
  117. * https://github.com/plone/Products.PlonePAS/tree/master/Products/PlonePAS/plugins/ufactory.py
  118. * https://github.com/plone/Products.PlonePAS/tree/master/Products/PlonePAS/plugins/group.py
  119. Creating a group
  120. ----------------
  121. Example::
  122. groups_tool = getToolByName(context, 'portal_groups')
  123. group_id = "companies"
  124. if not group_id in groups_tool.getGroupIds():
  125. groups_tool.addGroup(group_id)
  126. For more information, see:
  127. * https://github.com/plone/Products.PlonePAS/tree/master/Products/PlonePAS/tests/test_groupstool.py
  128. * https://github.com/plone/Products.PlonePAS/tree/master/Products/PlonePAS/plugins/group.py
  129. Add local roles to a group
  130. --------------------------
  131. Example::
  132. from AccessControl.interfaces import IRoleManager
  133. if IRoleManager.providedBy(context):
  134. context.manage_addLocalRoles(groupid, ['Manager',])
  135. .. Note:: This is an example of code in a *view*, where ``context`` is
  136. available.
  137. Update properties for a group
  138. -----------------------------
  139. The ``editGroup`` method modifies the title and description in the
  140. ``source_groups`` plugin, and subsequently calls ``setGroupProperties(kw)``
  141. which sets the properties on the ``mutable_properties`` plugin.
  142. Example::
  143. portal_groups.editGroup(groupid, **properties)
  144. portal_groups.editGroup(groupid, roles = ['Manager',])
  145. portal_groups.editGroup(groupid, title = u'my group title')
  146. Getting available groups
  147. ------------------------
  148. Getting all groups on the site is possible through ``acl_users`` and the
  149. ``source_groups`` plugin, which provides the functionality to manipulate
  150. Plone groups.
  151. Example to get only ids::
  152. acl_users = getToolByName(self, 'acl_users')
  153. # Iterable returning id strings:
  154. groups = acl_users.source_groups.getGroupIds()
  155. Example to get full group information::
  156. acl_users = getToolByName(self, 'acl_users')
  157. group_list = acl_users.source_groups.getGroups()
  158. for group in group_list:
  159. # group is PloneGroup object
  160. yield (group.getName(), group.title)
  161. Adding a user to a group
  162. ------------------------
  163. Example::
  164. # Add user to group "companies"
  165. portal_groups = getToolByName(self, 'portal_groups')
  166. portal_groups.addPrincipalToGroup(member.getUserName(), "companies")
  167. Removing a user from a group
  168. ------------------------------
  169. Example::
  170. portal_groups.removePrincipalFromGroup(member.getUserName(), "companies")
  171. Getting groups for a certain user
  172. ---------------------------------
  173. Below is an example of getting groups for the logged-in user (Plone 3 and
  174. earlier)::
  175. mt = getToolByName(self.context, 'portal_membership')
  176. mt.getAuthenticatedMember().getGroups()
  177. In Plone 4 you have to use::
  178. groups_tool = getToolByName(self, 'portal_groups')
  179. groups_tool.getGroupsByUserId('admin')
  180. Checking whether a user exists
  181. ===============================
  182. Example::
  183. mt = getToolByName(self, 'portal_membership')
  184. return mt.getMemberById(id) is None
  185. See also:
  186. * http://svn.zope.org/Products.CMFCore/trunk/Products/CMFCore/RegistrationTool.py?rev=110418&view=auto
  187. .. XXX: Why reference revision 110418 specifically?
  188. Creating users
  189. ===============
  190. Use the ``portal_registration`` tool. Example (browserview)::
  191. def createCompany(request, site, username, title, email, passwd=None):
  192. """
  193. Utility function which performs the actual creation, role and permission magic.
  194. @param username: Unicode string
  195. @param title: Fullname of user, unicode string
  196. @return: Created company content item or None if the creation fails
  197. """
  198. # If we use custom member properties they must be initialized
  199. # before regtool is called
  200. prepareMemberProperties(site)
  201. # portal_registration manages new user creation
  202. regtool = getToolByName(site, 'portal_registration')
  203. # Default password to the username
  204. # ... don't do this on the production server!
  205. if passwd == None:
  206. passwd = username
  207. # We allow only lowercase
  208. username = username.lower()
  209. # Username must be ASCII string
  210. # or Plone will choke when the user tries to log in
  211. try:
  212. username = str(username)
  213. except UnicodeEncodeError:
  214. IStatusMessage(request).addStatusMessage(_(u"Username must contain only characters a-z"), "error")
  215. return None
  216. # This is the minimum required information
  217. # to create a working member
  218. properties = {
  219. 'username': username,
  220. # Full name must always be utf-8 encoded
  221. 'fullname': title.encode("utf-8"),
  222. 'email': email
  223. }
  224. try:
  225. # addMember() returns MemberData object
  226. member = regtool.addMember(username, passwd, properties=properties)
  227. except ValueError, e:
  228. # Give user visual feedback what went wrong
  229. IStatusMessage(request).addStatusMessage(_(u"Could not create the user:") + unicode(e), "error")
  230. return None
  231. .. XXX: The unicode check above doesn't match the error message.
  232. Batch member creation
  233. -----------------------
  234. * http://plone.org/documentation/kb/batch-adding-users
  235. Email login
  236. ===========
  237. * Plone 3 does not allow a dot in the username.
  238. * This is available as an add-on; see http://plone.org/products/betahaus.emaillogin
  239. * In Plone 4, it is a default feature.
  240. Custom member creation form: complex example
  241. =============================================
  242. Below is an example of a Grok form which the administrator can use to create
  243. new users. New users will receive special properties and a folder for which
  244. they have ownership access. The password is set to be the same as the
  245. username. The user is added to a group named "companies".
  246. Example ``company.py``::
  247. """ Add companies.
  248. Create user account + associated "home folder" content type
  249. for a company user.
  250. User accounts have a special role.
  251. Note: As of this writing, in 2010-04, we need the
  252. plone.app.directives trunk version which
  253. contains an unreleased validation decorator.
  254. """
  255. # Core Zope 2 + Zope 3 + Plone
  256. from zope.interface import Interface
  257. from zope import schema
  258. from five import grok
  259. from Products.CMFCore.interfaces import ISiteRoot
  260. from Products.CMFCore.utils import getToolByName
  261. from Products.CMFCore import permissions
  262. from Products.statusmessages.interfaces import IStatusMessage
  263. # Form and validation
  264. from z3c.form import field
  265. import z3c.form.button
  266. from plone.directives import form
  267. from collective.z3cform.grok.grok import PloneFormWrapper
  268. import plone.autoform.form
  269. # Products.validation uses some ugly ZService magic which I can't quite comprehend
  270. from Products.validation import validation
  271. # Our translation catalog
  272. from zope.i18nmessageid import MessageFactory
  273. OurMessageFactory = MessageFactory('OurProduct')
  274. OurMessageFactory = _
  275. # If we're building an addon, we may already have one, for example:
  276. # from isleofback.app import appMessageFactory as _
  277. grok.templatedir("templates")
  278. class ICompanyCreationFormSchema(form.Schema):
  279. """ Define fields used on the form """
  280. username = schema.TextLine(title=u"Username")
  281. company_name = schema.TextLine(title=u"Company name")
  282. email = schema.TextLine(title=u"Email")
  283. class CompanyCreationForm(plone.autoform.form.AutoExtensibleForm, form.Form):
  284. """ Form action controller.
  285. form.DisplayForm will automatically expose the form
  286. as a view, no wrapping view creation needed.
  287. """
  288. # Form label
  289. name = _(u"Create Company")
  290. # Which schema is used by AutoExtensibleForm
  291. schema = ICompanyCreationFormSchema
  292. # The form does not care about the context object
  293. # and should not try to extract field value
  294. # defaults out of it
  295. ignoreContext = True
  296. # This form is available at the site root only
  297. grok.context(ISiteRoot)
  298. # z3c.form has a function decorator
  299. # which turns the function to a form button action handler
  300. @z3c.form.button.buttonAndHandler(_('Create Company'), name='create')
  301. def createCompanyAction(self, action):
  302. """ Button action handler to create company.
  303. """
  304. data, errors = self.extractData()
  305. if errors:
  306. self.status = self.formErrorsMessage
  307. return
  308. obj = createCompany(self.request, self.context, data["username"], data["company_name"], data["email"])
  309. if obj is not None:
  310. # mark as finished only if we get the new object
  311. IStatusMessage(self.request).addStatusMessage(_(u"Company created"), "info")
  312. class CompanyCreationView(PloneFormWrapper):
  313. """ View which exposes form as URL """
  314. form = CompanyCreationForm
  315. # Set up security barrier -
  316. # non-priviledged users can't access this form
  317. grok.require("cmf.ManagePortal")
  318. # Use http://yourhost/@@create_company URL to access this form
  319. grok.name("create_company")
  320. # This view is available at the site root only
  321. grok.context(ISiteRoot)
  322. # Which template is used to decorate the form
  323. # -> forms.pt in template directory
  324. grok.template("form")
  325. @form.validator(field=ICompanyCreationFormSchema['email'])
  326. def validateEmail(value):
  327. """ Use old Products.validation validators to perform the validation.
  328. """
  329. validator_function = validation.validatorFor('isEmail')
  330. if not validator_function(value):
  331. raise schema.ValidationError(u"Entered email address is not good:" + value)
  332. def prepareMemberProperties(site):
  333. """ Adjust site for custom member properties """
  334. # Need to use ancient Z2 property sheet API here...
  335. portal_memberdata = getToolByName(site, "portal_memberdata")
  336. # When new member is created, its MemberData
  337. # is populated with the values from portal_memberdata property sheet,
  338. # so value="" will be the default value for users' home_folder_uid
  339. # member property
  340. if not portal_memberdata.hasProperty("home_folder_uid"):
  341. portal_memberdata.manage_addProperty(id="home_folder_uid", value="", type="string")
  342. # Create a group "companies" where newly created members will be added
  343. acl_users = getToolByName(site, 'acl_users')
  344. gt = getToolByName(site, 'portal_groups')
  345. group_id = "companies"
  346. if not group_id in gt.getGroupIds():
  347. gt.addGroup(group_id, [], [], {'title': 'Companies'})
  348. def createCompany(request, site, username, title, email, passwd=None):
  349. """
  350. Utility function which performs the actual creation, role and permission magic.
  351. @param username: Unicode string
  352. @param title: Fullname of user, unicode string
  353. @return: Created company content item or None if the creation fails
  354. """
  355. # If we use custom member properties
  356. # they must be intiialized before regtool is called
  357. prepareMemberProperties(site)
  358. # portal_registrations manages new user creation
  359. regtool = getToolByName(site, 'portal_registration')
  360. # Default password to the username
  361. # ... don't do this on the production server!
  362. if passwd == None:
  363. passwd = username
  364. # Only lowercase allowed
  365. username = username.lower()
  366. # Username must be ASCII string
  367. # or Plone will choke when the user tries to log in
  368. try:
  369. username = str(username)
  370. except UnicodeEncodeError:
  371. IStatusMessage(request).addStatusMessage(_(u"Username must contain only characters a-z"), "error")
  372. return None
  373. # This is minimum required information set
  374. # to create a working member
  375. properties = {
  376. 'username': username,
  377. # Full name must be always as utf-8 encoded
  378. 'fullname': title.encode("utf-8"),
  379. 'email': email
  380. }
  381. try:
  382. # addMember() returns MemberData object
  383. member = regtool.addMember(username, passwd, properties=properties)
  384. except ValueError, e:
  385. # Give user visual feedback what went wrong
  386. IStatusMessage(request).addStatusMessage(_(u"Could not create the user:") + unicode(e), "error")
  387. return None
  388. # Add user to group "companies"
  389. gt = getToolByName(site, 'portal_groups')
  390. gt.addPrincipalToGroup(member.getUserName(), "companies")
  391. return createMatchingHomeFolder(request, site, member)
  392. def createMatchingHomeFolder(request, site, member, target_folder="yritykset", target_type="IsleofbackCompany", language="fi"):
  393. """ Creates a folder, sets its ownership for the member and stores the folder UID in the member data.
  394. @param member: MemberData object
  395. @param target_folder: Under which folder a new content item is created
  396. @param language: Initial two language code of the item
  397. """
  398. parent_folder = site.restrictedTraverse(target_folder)
  399. # Cannot add custom memberdata properties unless explicitly declared
  400. id = member.getUserName()
  401. parent_folder.invokeFactory(target_type, id)
  402. home_folder = parent_folder[id]
  403. name = member.getProperty("fullname")
  404. home_folder.setTitle(name)
  405. home_folder.setLanguage(language)
  406. email = member.getProperty("email")
  407. home_folder.setEmail(email)
  408. # Unset the Archetypes object creation flag
  409. home_folder.processForm()
  410. # Store UID of the created folder in memberdata so we can
  411. # look it up later to e.g. generate the link to the member folder
  412. member.setMemberProperties({"home_folder_uid": home_folder.UID()})
  413. # Get the user handle from member data object
  414. user = member.getUser()
  415. username = user.getUserName()
  416. home_folder.manage_setLocalRoles(username, ["Owner",])
  417. home_folder.reindexObjectSecurity()
  418. return home_folder