PageRenderTime 305ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/Products/listen/extras/widgets.py

https://github.com/socialplanning/opencore-listen
Python | 309 lines | 282 code | 10 blank | 17 comment | 8 complexity | 7107be1b4d3de9ebf4c60dca5a2b0872 MD5 | raw file
  1. # Specialized Zope 3 widgets for listen
  2. import itertools
  3. from zope.component import getMultiAdapter
  4. from zope.component.interfaces import ComponentLookupError
  5. from zope.interface import Interface
  6. from zope.app.form.browser.widget import SimpleInputWidget
  7. from zope.app.form.browser.widget import DisplayWidget
  8. from zope.app.form.interfaces import IInputWidget
  9. from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
  10. from Products.CMFCore.utils import getToolByName
  11. from Products.CMFPlone.utils import getSiteEncoding
  12. from Products.listen.lib.common import email_regex
  13. class MemberBaseWidget(object):
  14. """A mixin class for widgets that need to deal with Plone member objects"""
  15. def getMemberInfoFromId(self, mid):
  16. mtool = getToolByName(self.context.context, 'portal_membership')
  17. portal_url = getToolByName(mtool, 'portal_url')()
  18. portal_url = portal_url.endswith('/') and portal_url or portal_url + '/'
  19. author_url = portal_url + 'author/'
  20. # Plone 2.5 does not like the id to be unicode
  21. encoding = getSiteEncoding(self.context.context)
  22. mid = mid.encode(encoding)
  23. m = mtool.getMemberById(mid)
  24. name = m.getUserName()
  25. return {'id': m.getId(),
  26. 'name': name,
  27. 'fullname': m.getProperty('fullname', ''),
  28. 'url': author_url+name}
  29. class MemberRemovalWidget(SimpleInputWidget, MemberBaseWidget):
  30. """A simple widget that displays a list of current members and allows
  31. removing them (but not adding them). This is useful as a base for the
  32. search widget, and also in an invitation based list, where members are
  33. invited using a search widget, and current members can only be removed,
  34. not added directly.
  35. We need to setup a Tuple field and use this widget for it, as well as some
  36. members::
  37. >>> from Products.listen.extras.tests import setupBasicFieldRequestAndMembers
  38. >>> field, request = setupBasicFieldRequestAndMembers(self.portal)
  39. >>> from Products.listen.extras.widgets import MemberRemovalWidget
  40. >>> widget = MemberRemovalWidget(field, field.value_type, request)
  41. >>> widget.name
  42. 'field.foo'
  43. Let's examine our currently empty field::
  44. >>> widget.getData()
  45. []
  46. >>> widget.hidden()
  47. ''
  48. Data about current users will be available to the browser as a
  49. mapping with user data::
  50. >>> widget.setRenderedValue((u'test1',))
  51. >>> widget.getData()
  52. [{'url': 'http://nohost/plone/author/test1', 'fullname': 'Test User 1', 'id': 'test1', 'name': 'test1'}]
  53. The hidden widget should be hidden, we need to load the standard
  54. form zcml for this to work::
  55. >>> from Products.Five import zcml
  56. >>> from zope.app.form import browser
  57. >>> zcml.load_config('configure.zcml', package=browser)
  58. >>> print widget.hidden()
  59. <input class="hiddenType" id="field.foo" name="field.foo" type="hidden" value="test1" />
  60. Data returned from the form to the widget will be in the expected
  61. form (in this case a list of strigs should become a tuple of
  62. unicode strings)::
  63. >>> widget.request.form[widget.name] = ['test1']
  64. >>> widget.getInputValue()
  65. (u'test1',)
  66. There's a sneaky little trick, where a marker value is set as the first value
  67. in the list to ensure that an empty list still appears in the request::
  68. >>> widget.request.form[widget.name] = [widget.marker, 'test1']
  69. >>> widget.getInputValue()
  70. (u'test1',)
  71. >>> widget.request.form[widget.name] = [widget.marker]
  72. >>> widget.getInputValue()
  73. ()
  74. Another sneaky trick is that we silently enforce the rule that no entries
  75. may be added, only removed with this widget by filtering the values::
  76. >>> widget.request.form[widget.name] = [widget.marker, 'test1', 'test2']
  77. >>> widget.getInputValue()
  78. (u'test1',)
  79. Should perform postback from the request if no data is available, but enforce
  80. the restrictions on the postback. If the widget data is unset the
  81. widget consults the field context to get a value, so we must set an
  82. appropriate attribute on the field:
  83. >>> widget.setRenderedValue(widget._data_marker)
  84. >>> widget.getData()
  85. []
  86. >>> request.form[widget.name] = [widget.marker, 'test1', 'test2']
  87. >>> widget.getData()
  88. []
  89. >>> setattr(self.portal, field.__name__, (u'test1',))
  90. >>> widget.getData()
  91. [{'url': 'http://nohost/plone/author/test1', 'fullname': 'Test User 1', 'id': 'test1', 'name': 'test1'}]
  92. """
  93. template = ViewPageTemplateFile('member_removal.pt')
  94. marker = '__marker'
  95. def __init__(self, context, value_type, request):
  96. """Initialize the widget."""
  97. # only allow this to happen for a bound field
  98. assert context.context is not None
  99. self._type = context._type
  100. self._sub_type = value_type._type
  101. super(MemberRemovalWidget, self).__init__(context, request)
  102. def __call__(self):
  103. self.request.debug = 0
  104. return self.template()
  105. def getData(self):
  106. if not self._renderedValueSet():
  107. # Pull the values from the request if available, e.g. on resubmit
  108. data = self._toFieldValue(self.request.get(self.name, []))
  109. else:
  110. data = self._data
  111. return [self.getMemberInfoFromId(m) for m in data]
  112. def _toFieldValue(self, input_vals):
  113. """Coerce the input value to the expected sequence type for
  114. getInputValue"""
  115. # In we have a hidden field in the form that ensures that the list
  116. # is always submitted, even when empty. We must remove that value.
  117. if input_vals and self.marker == input_vals[0]:
  118. input_vals.pop(0)
  119. input_vals = super(MemberRemovalWidget, self)._toFieldValue(input_vals)
  120. input_vals = [self._sub_type(i) for i in input_vals]
  121. # Perform any filtering:
  122. input_vals = self._restrictInputValues(input_vals)
  123. return self._type(input_vals)
  124. def _restrictInputValues(self, input_vals):
  125. """This widget has a contract that it will not allow for adding new
  126. entries, only removing existing ones, we must enforce this."""
  127. if not self._renderedValueSet():
  128. # Get the current values of the field from the adapted bound context
  129. # as self._data may not have been set yet.
  130. try:
  131. # Get the value directly from the context of the field
  132. context = self.context.context
  133. if self.context.interface is not None:
  134. # if the field has an associated inteface/schema attempt to
  135. # adapt the context to it
  136. try:
  137. context = self.context.interface(context)
  138. except ComponentLookupError:
  139. pass
  140. cur_vals = self.context.get(context)
  141. except AttributeError:
  142. return []
  143. else:
  144. cur_vals = self._data
  145. cur_vals = dict(itertools.izip(cur_vals,
  146. itertools.repeat(None)))
  147. return [val for val in input_vals if val in cur_vals]
  148. def hidden(self):
  149. """Render the widget as hidden fields"""
  150. fields = []
  151. data = self._renderedValueSet() and self._data or []
  152. for value in data:
  153. widget = getMultiAdapter((self.context.value_type, self.request),
  154. IInputWidget)
  155. widget.name = self.name
  156. widget.setRenderedValue(value)
  157. fields.append(widget.hidden())
  158. return '\n'.join(fields)
  159. class MemberSearchWidget(MemberRemovalWidget):
  160. """An input widget for searching for members and adding them to a
  161. list. Let's setup a widget for a Tuple field, and a portal with
  162. some members::
  163. >>> from Products.listen.extras.tests import setupBasicFieldRequestAndMembers
  164. >>> field, request = setupBasicFieldRequestAndMembers(self.portal)
  165. >>> from Products.listen.extras.widgets import MemberSearchWidget
  166. >>> widget = MemberSearchWidget(field, field.value_type, request)
  167. >>> widget.name
  168. 'field.foo'
  169. Let's examine our currently empty field::
  170. >>> widget.getData()
  171. []
  172. >>> widget.hidden()
  173. ''
  174. And the search method of the widget (for when Ajax is not
  175. available). In order to use the search results, we must register a 'view'::
  176. >>> from zope.component import provideAdapter
  177. >>> from zope.interface import Interface
  178. >>> from Products.listen.extras.member_search import MemberSearchView
  179. >>> provideAdapter(MemberSearchView, (Interface, Interface),
  180. ... name='member_search.html')
  181. >>> widget.request.form['field.foo.search_term'] = 'test1'
  182. >>> widget.request.form['field.foo.search_param'] = 'name'
  183. >>> widget.getSearchResults()
  184. [{'url': 'http://nohost/plone/author/test1', 'fullname': 'Test User 1', 'id': 'test1', 'name': 'test1'}]
  185. However, if the field has values already those values will not be
  186. shown in the search results::
  187. >>> widget.setRenderedValue((u'test1',))
  188. >>> widget.getSearchResults()
  189. []
  190. >>> widget.request.form['field.foo.search_term'] = 'test2'
  191. >>> widget.getSearchResults()
  192. [{'url': 'http://nohost/plone/author/test2', 'fullname': 'Test User 2', 'id': 'test2', 'name': 'test2'}]
  193. This widget needs to allow adding new values, unlike its parent widget::
  194. >>> widget.request.form[widget.name] = [widget.marker, 'test1', 'test2']
  195. >>> widget.getInputValue()
  196. (u'test1', u'test2')
  197. """
  198. template = ViewPageTemplateFile('member_search.pt')
  199. marker = '__marker'
  200. def getSearchResults(self):
  201. """Use the member search view to get member search results directly"""
  202. portal = getToolByName(self.context.context,
  203. 'portal_url').getPortalObject()
  204. view = getMultiAdapter((portal, self.request),
  205. name='member_search.html')
  206. self.request.form['search_term'] = \
  207. self.request.get(self.name+'.search_term', '')
  208. self.request.form['search_type'] = \
  209. self.request.get(self.name+'.search_param', 'name')
  210. # Don't return results we already have
  211. cur_vals = [m['id'] for m in self.getData()]
  212. return [self.getMemberInfoFromId(i.getId()) for
  213. i in view.searchForMembers() if i.getId() not in cur_vals]
  214. def _restrictInputValues(self, input):
  215. """This widget allows adding entries, so no filtering"""
  216. return input
  217. class SubscriberRemovalWidget(MemberRemovalWidget):
  218. """A version of the removal widget that will accept entries that may be
  219. simple email addresses and not exclusively member ids.
  220. We again setup a Tuple field and use this widget for it, as well as some
  221. members::
  222. >>> from Products.listen.extras.tests import setupBasicFieldRequestAndMembers
  223. >>> field, request = setupBasicFieldRequestAndMembers(self.portal)
  224. >>> from Products.listen.extras.widgets import SubscriberRemovalWidget
  225. >>> widget = SubscriberRemovalWidget(field, field.value_type, request)
  226. >>> widget.name
  227. 'field.foo'
  228. Data about members should work as before::
  229. >>> widget.setRenderedValue((u'test1',))
  230. >>> widget.getData()
  231. [{'url': 'http://nohost/plone/author/test1', 'fullname': 'Test User 1', 'id': 'test1', 'name': 'test1'}]
  232. But, if we add a simple email address::
  233. >>> widget.setRenderedValue((u'test1','tester@example.com'))
  234. >>> widget.getData()
  235. [{'url': 'http://nohost/plone/author/test1', 'fullname': 'Test User 1', 'id': 'test1', 'name': 'test1'}, {'url': 'mailto:tester@example.com', 'fullname': '', 'id': 'tester@example.com', 'name': 'tester@example.com'}]
  236. """
  237. def getData(self):
  238. if not self._renderedValueSet():
  239. # Pull the values from the request if available, e.g. on resubmit
  240. data = self._toFieldValue(self.request.get(self.name, []))
  241. else:
  242. data = self._data
  243. entries = []
  244. for entry in data:
  245. try:
  246. info = self.getMemberInfoFromId(entry)
  247. entries.append(info)
  248. except AttributeError:
  249. info = None
  250. if info is None and email_regex.match(entry):
  251. info = {'id': entry, 'name': entry, 'fullname': '',
  252. 'url': 'mailto:'+entry}
  253. entries.append(info)
  254. return entries
  255. class MemberListDisplayWidget(DisplayWidget, MemberBaseWidget):
  256. """A display widget for showing lists of Plone member objects."""
  257. item_str = '<li><a href="%(url)s">%(fullname)s (%(name)s)</a></li>'
  258. def __call__(self):
  259. view_items = ['<ul>']
  260. if self._renderedValueSet():
  261. value = self._data
  262. else:
  263. value = self.context.default
  264. if value == self.context.missing_value:
  265. return ""
  266. for item in value:
  267. view_items.append(self.item_str%self.getMemberInfoFromId(item))
  268. view_items.append['</ul>']
  269. return '\n'.join(view_items)