[Testbot] Plone 5.0 - Python 2.7 - Build # 1887 - Regression! - 7 failure(s)
jenkins at plone.org
jenkins at plone.org
Sat Mar 8 13:58:38 UTC 2014
-------------------------------------------------------------------------------
Plone 5.0 - Python 2.7 - Build # 1887 - Failure!
-------------------------------------------------------------------------------
http://jenkins.plone.org/job/plone-5.0-python-2.7/1887/
-------------------------------------------------------------------------------
CHANGES
-------------------------------------------------------------------------------
Repository: plone.app.widgets
Branch: refs/heads/master
Date: 2014-03-07T15:17:44-08:00
Author: David Glick (davisagli) <david at glicksoftware.com>
Commit: https://github.com/plone/plone.app.widgets/commit/f1ec347884c621b7c66c3ba498d38614967ba284
add support for querying sources to the ajax select and related items widgets
Files changed:
M plone/app/widgets/browser/configure.zcml
M plone/app/widgets/browser/vocabulary.py
M plone/app/widgets/configure.zcml
M plone/app/widgets/dx.py
M plone/app/widgets/tests/test_browser.py
M plone/app/widgets/tests/test_dx.py
diff --git a/plone/app/widgets/browser/configure.zcml b/plone/app/widgets/browser/configure.zcml
index fb4fbd1..d681e53 100644
--- a/plone/app/widgets/browser/configure.zcml
+++ b/plone/app/widgets/browser/configure.zcml
@@ -14,6 +14,13 @@
/>
<browser:page
+ name="getSource"
+ for="z3c.form.interfaces.IWidget"
+ class=".vocabulary.SourceView"
+ permission="zope.Public"
+ />
+
+ <browser:page
name="fileUpload"
for="Products.CMFCore.interfaces._content.IFolderish"
class=".file.FileUploadView"
diff --git a/plone/app/widgets/browser/vocabulary.py b/plone/app/widgets/browser/vocabulary.py
index c83d82f..e1cc615 100644
--- a/plone/app/widgets/browser/vocabulary.py
+++ b/plone/app/widgets/browser/vocabulary.py
@@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager
+from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.Five import BrowserView
-from Products.ZCTextIndex.ParseTree import ParseError
from logging import getLogger
-from plone.app.vocabularies.interfaces import ISlicableVocabulary
+from plone.app.querystring import queryparser
from plone.app.widgets.interfaces import IFieldPermissionChecker
+from plone.autoform.interfaces import WRITE_PERMISSIONS_KEY
+from plone.supermodel.utils import mergedTaggedValueDict
from types import FunctionType
+from zope.component import getUtility
from zope.component import queryAdapter
from zope.component import queryUtility
+from zope.schema.interfaces import ICollection
from zope.schema.interfaces import IVocabularyFactory
-
+from zope.security.interfaces import IPermission
import inspect
+import itertools
import json
logger = getLogger(__name__)
@@ -39,14 +44,11 @@ def _parseJSON(s):
_safe_callable_metadata = ['getURL', 'getPath']
-class VocabularyView(BrowserView):
+class VocabLookupException(Exception):
+ pass
- def error(self):
- return json.dumps({
- 'results': [],
- 'total': 0,
- 'error': True
- })
+
+class BaseVocabularyView(BrowserView):
def __call__(self):
"""
@@ -66,89 +68,57 @@ def __call__(self):
size: size of paged results
}
"""
- context = self.context
+ context = self.get_context()
self.request.response.setHeader("Content-type", "application/json")
- factory_name = self.request.get('name', None)
- field_name = self.request.get('field', None)
- if not factory_name:
- return json.dumps({'error': 'No factory provided.'})
- authorized = None
- sm = getSecurityManager()
- if (factory_name not in _permissions or
- not IPloneSiteRoot.providedBy(context)):
- # Check field specific permission
- if field_name:
- permission_checker = queryAdapter(context,
- IFieldPermissionChecker)
- if permission_checker is not None:
- authorized = permission_checker.validate(field_name,
- factory_name)
- if not authorized:
- return json.dumps({'error': 'Vocabulary lookup not allowed'})
- # Short circuit if we are on the site root and permission is
- # in global registry
- elif not sm.checkPermission(_permissions[factory_name], context):
- return json.dumps({'error': 'Vocabulary lookup not allowed'})
-
- factory = queryUtility(IVocabularyFactory, factory_name)
- if not factory:
- return json.dumps({
- 'error': 'No factory with name "%s" exists.' % factory_name})
-
- # check if factory accepts query argument
- query = _parseJSON(self.request.get('query', ''))
- batch = _parseJSON(self.request.get('batch', ''))
-
- if type(factory) is FunctionType:
- factory_spec = inspect.getargspec(factory)
- else:
- factory_spec = inspect.getargspec(factory.__call__)
try:
- supports_batch = False
- vocabulary = None
- if query and 'query' in factory_spec.args:
- if 'batch' in factory_spec.args:
- vocabulary = factory(self.context,
- query=query, batch=batch)
- supports_batch = True
- else:
- vocabulary = factory(self.context, query=query)
- elif query:
- raise KeyError("The vocabulary factory %s does not support "
- "query arguments",
- factory)
-
- if batch and supports_batch:
- vocabulary = factory(context, query, batch)
- elif query:
- vocabulary = factory(context, query)
- else:
- vocabulary = factory(context)
+ vocabulary = self.get_vocabulary()
+ except VocabLookupException, e:
+ return json.dumps({'error': e.message})
- except (TypeError, ParseError):
- raise
- return self.error()
+ results_are_brains = False
+ if hasattr(vocabulary, 'search_catalog'):
+ query = self.parsed_query()
+ results = vocabulary.search_catalog(query)
+ results_are_brains = True
+ elif hasattr(vocabulary, 'search'):
+ try:
+ query = self.parsed_query()['SearchableText']['query']
+ except KeyError:
+ results = iter(vocabulary)
+ else:
+ results = vocabulary.search(query)
+ else:
+ results = vocabulary
try:
- total = len(vocabulary)
+ total = len(results)
except TypeError:
total = 0 # do not error if object does not support __len__
# we'll check again later if we can figure some size
# out
+
+ # get batch
+ batch = _parseJSON(self.request.get('batch', ''))
if batch and ('size' not in batch or 'page' not in batch):
batch = None # batching not providing correct options
- logger.error("A vocabulary request contained bad batch "
- "information. The batch information is ignored.")
- if batch and not supports_batch and \
- ISlicableVocabulary.providedBy(vocabulary):
+ if batch:
# must be slicable for batching support
page = int(batch['page'])
# page is being passed in is 1-based
- start = (max(page-1, 0)) * int(batch['size'])
+ start = (max(page - 1, 0)) * int(batch['size'])
end = start + int(batch['size'])
- vocabulary = vocabulary[start:end]
+ # Try __getitem__-based slice, then iterator slice.
+ # The iterator slice has to consume the iterator through
+ # to the desired slice, but that shouldn't be the end
+ # of the world because at some point the user will hopefully
+ # give up scrolling and search instead.
+ try:
+ results = results[start:end]
+ except TypeError:
+ results = itertools.islice(results, start, end)
+ # build result items
items = []
attributes = _parseJSON(self.request.get('attributes', ''))
@@ -156,8 +126,11 @@ def __call__(self):
attributes = attributes.split(',')
if attributes:
- base_path = '/'.join(context.getPhysicalPath())
- for vocab_item in vocabulary:
+ portal = getToolByName(context, 'portal_url').getPortalObject()
+ base_path = '/'.join(portal.getPhysicalPath())
+ for vocab_item in results:
+ if not results_are_brains:
+ vocab_item = vocab_item.value
item = {}
for attr in attributes:
key = attr
@@ -167,8 +140,7 @@ def __call__(self):
continue
if key == 'path':
attr = 'getPath'
- vocab_value = vocab_item.value
- val = getattr(vocab_value, attr, None)
+ val = getattr(vocab_item, attr, None)
if callable(val):
if attr in _safe_callable_metadata:
val = val()
@@ -179,7 +151,7 @@ def __call__(self):
item[key] = val
items.append(item)
else:
- for item in vocabulary:
+ for item in results:
items.append({'id': item.token, 'text': item.title})
if total == 0:
@@ -189,3 +161,97 @@ def __call__(self):
'results': items,
'total': total
})
+
+ def parsed_query(self, ):
+ query = _parseJSON(self.request.get('query', '')) or {}
+ if query:
+ parsed = queryparser.parseFormquery(
+ self.get_context(), query['criteria'])
+ if 'sort_on' in query:
+ parsed['sort_on'] = query['sort_on']
+ if 'sort_order' in query:
+ parsed['sort_order'] = str(query['sort_order'])
+ query = parsed
+ return query
+
+
+class VocabularyView(BaseVocabularyView):
+ """Queries a named vocabulary and returns JSON-formatted results."""
+
+ def get_context(self):
+ return self.context
+
+ def get_vocabulary(self):
+ # Look up named vocabulary and check permission.
+
+ context = self.context
+ factory_name = self.request.get('name', None)
+ field_name = self.request.get('field', None)
+ if not factory_name:
+ raise VocabLookupException('No factory provided.')
+ authorized = None
+ sm = getSecurityManager()
+ if (factory_name not in _permissions or
+ not IPloneSiteRoot.providedBy(context)):
+ # Check field specific permission
+ if field_name:
+ permission_checker = queryAdapter(context,
+ IFieldPermissionChecker)
+ if permission_checker is not None:
+ authorized = permission_checker.validate(field_name,
+ factory_name)
+ if not authorized:
+ raise VocabLookupException('Vocabulary lookup not allowed')
+ # Short circuit if we are on the site root and permission is
+ # in global registry
+ elif not sm.checkPermission(_permissions[factory_name], context):
+ raise VocabLookupException('Vocabulary lookup not allowed')
+
+ factory = queryUtility(IVocabularyFactory, factory_name)
+ if not factory:
+ raise VocabLookupException(
+ 'No factory with name "%s" exists.' % factory_name)
+
+ # This part is for backwards-compatibility with the first
+ # generation of vocabularies created for plone.app.widgets,
+ # which take the (unparsed) query as a parameter of the vocab
+ # factory rather than as a separate search method.
+ if type(factory) is FunctionType:
+ factory_spec = inspect.getargspec(factory)
+ else:
+ factory_spec = inspect.getargspec(factory.__call__)
+ query = _parseJSON(self.request.get('query', ''))
+ if query and 'query' in factory_spec.args:
+ vocabulary = factory(context, query=query)
+ else:
+ # This is what is reached for non-legacy vocabularies.
+ vocabulary = factory(context)
+
+ return vocabulary
+
+
+class SourceView(BaseVocabularyView):
+ """Queries a field's source and returns JSON-formatted results."""
+
+ def get_context(self):
+ return self.context.context
+
+ def get_vocabulary(self):
+ widget = self.context
+ field = widget.field.bind(widget.context)
+
+ # check field's write permission
+ info = mergedTaggedValueDict(field.interface, WRITE_PERMISSIONS_KEY)
+ permission_name = info.get(field.__name__, 'cmf.ModifyPortalContent')
+ permission = queryUtility(IPermission, name=permission_name)
+ if permission is None:
+ permission = getUtility(
+ IPermission, name='cmf.ModifyPortalContent')
+ if not getSecurityManager().checkPermission(
+ permission.title, self.get_context()):
+ raise VocabLookupException('Vocabulary lookup not allowed.')
+
+ if ICollection.providedBy(field):
+ return field.value_type.vocabulary
+ else:
+ return field.vocabulary
diff --git a/plone/app/widgets/configure.zcml b/plone/app/widgets/configure.zcml
index 5f5809c..4cf39ae 100644
--- a/plone/app/widgets/configure.zcml
+++ b/plone/app/widgets/configure.zcml
@@ -156,6 +156,7 @@
<adapter factory=".dx.SelectWidgetConverter" />
<adapter factory=".dx.AjaxSelectWidgetConverter" />
<adapter factory=".dx.QueryStringDataConverter" />
+ <adapter factory=".dx.RelationChoiceRelatedItemsWidgetConverter" />
<adapter factory=".dx.RelatedItemsDataConverter" />
</configure>
@@ -236,6 +237,12 @@
z3c.form.interfaces.IFormLayer"
/>
+ <adapter
+ factory=".dx.RelatedItemsFieldWidget"
+ for="zope.schema.interfaces.IChoice
+ plone.app.vocabularies.catalog.CatalogSource
+ z3c.form.interfaces.IFormLayer" />
+
<adapter factory=".dx.QueryStringFieldWidget" />
<adapter factory=".dx.RichTextFieldWidget" />
diff --git a/plone/app/widgets/dx.py b/plone/app/widgets/dx.py
index c3a3643..67aa472 100644
--- a/plone/app/widgets/dx.py
+++ b/plone/app/widgets/dx.py
@@ -53,6 +53,7 @@
from zope.interface import implementer
from zope.interface import implements
from zope.interface import implementsOnly
+from zope.interface import Interface
from zope.publisher.browser import TestRequest
from zope.schema.interfaces import IChoice
from zope.schema.interfaces import ICollection
@@ -73,6 +74,17 @@
HAS_PAC = False
+try:
+ from z3c.relationfield.interfaces import IRelationChoice
+ from z3c.relationfield.interfaces import IRelationList
+except ImportError: # pragma: no cover
+ class IRelationChoice(Interface):
+ pass
+
+ class IRelationList(Interface):
+ pass
+
+
class IDateField(IDate):
"""Marker interface for the DateField."""
@@ -232,7 +244,8 @@ def toFieldValue(self, value):
class AjaxSelectWidgetConverter(BaseDataConverter):
- """Data converter for ICollection."""
+ """Data converter for ICollection fields using the AjaxSelectWidget.
+ """
adapts(ICollection, IAjaxSelectWidget)
@@ -272,8 +285,34 @@ def toFieldValue(self, value):
for v in value.split(separator))
+class RelationChoiceRelatedItemsWidgetConverter(BaseDataConverter):
+ """Data converter for RelationChoice fields using the RelatedItemsWidget.
+ """
+
+ adapts(IRelationChoice, IRelatedItemsWidget)
+
+ def toWidgetValue(self, value):
+ if not value:
+ return self.field.missing_value
+ return IUUID(value)
+
+ def toFieldValue(self, value):
+ if not value:
+ return self.field.missing_value
+ try:
+ catalog = getToolByName(self.widget.context, 'portal_catalog')
+ except AttributeError:
+ catalog = getToolByName(getSite(), 'portal_catalog')
+
+ res = catalog(UID=value)
+ if res:
+ return res[0].getObject()
+ else:
+ return self.field.missing_value
+
+
class RelatedItemsDataConverter(BaseDataConverter):
- """Data converter for ICollection."""
+ """Data converter for ICollection fields using the RelatedItemsWidget."""
adapts(ICollection, IRelatedItemsWidget)
@@ -289,7 +328,10 @@ def toWidgetValue(self, value):
if not value:
return self.field.missing_value
separator = getattr(self.widget, 'separator', ';')
- return separator.join([IUUID(o) for o in value if value])
+ if IRelationList.providedBy(self.field):
+ return separator.join([IUUID(o) for o in value if value])
+ else:
+ return separator.join(v for v in value if v)
def toFieldValue(self, value):
"""Converts from widget value to field.
@@ -300,24 +342,26 @@ def toFieldValue(self, value):
:returns: List of content objects
:rtype: list | tuple | set
"""
+ if not value:
+ return self.field.missing_value
+
collectionType = self.field._type
if isinstance(collectionType, tuple):
collectionType = collectionType[-1]
- if not len(value):
- return self.field.missing_value
-
separator = getattr(self.widget, 'separator', ';')
value = value.split(separator)
- value = [v.split('/')[0] for v in value]
- try:
- catalog = getToolByName(self.widget.context, 'portal_catalog')
- except AttributeError:
- catalog = getToolByName(getSite(), 'portal_catalog')
+ if IRelationList.providedBy(self.field):
+ try:
+ catalog = getToolByName(self.widget.context, 'portal_catalog')
+ except AttributeError:
+ catalog = getToolByName(getSite(), 'portal_catalog')
- return collectionType(item.getObject()
- for item in catalog(UID=value) if item)
+ return collectionType(item.getObject()
+ for item in catalog(UID=value) if item)
+ else:
+ return collectionType(v for v in value)
class QueryStringDataConverter(BaseDataConverter):
@@ -578,11 +622,6 @@ class AjaxSelectWidget(BaseWidget, z3cform_TextWidget):
vocabulary_view = '@@getVocabulary'
orderable = False
- def update(self, *args, **kwargs):
- if not hasattr(self, 'vocabulary'):
- self.vocabulary = getattr(self.field, 'vocabularyName', None)
- z3cform_TextWidget.update(self, *args, **kwargs)
-
def _base_args(self):
"""Method which will calculate _base class arguments.
@@ -610,12 +649,27 @@ def _base_args(self):
if IAddForm.providedBy(getattr(self, 'form')):
context = self.form
+ vocabulary_name = self.vocabulary
+ field = None
+ if IChoice.providedBy(self.field):
+ args['pattern_options']['maximumSelectionSize'] = 1
+ field = self.field
+ elif ICollection.providedBy(self.field):
+ field = self.field.value_type
+ if not vocabulary_name and field is not None:
+ vocabulary_name = field.vocabularyName
+
args['pattern_options'] = dict_merge(
get_ajaxselect_options(context, args['value'], self.separator,
- self.vocabulary, self.vocabulary_view,
+ vocabulary_name, self.vocabulary_view,
field_name),
args['pattern_options'])
+ if field and getattr(field, 'vocabulary', None):
+ form_url = self.request.getURL()
+ source_url = "%s/++widget++%s/@@getSource" % (form_url, self.name)
+ args['pattern_options']['vocabularyUrl'] = source_url
+
# ISequence represents an orderable collection
if ISequence.providedBy(self.field) or self.orderable:
args['pattern_options']['orderable'] = True
@@ -634,20 +688,10 @@ class RelatedItemsWidget(BaseWidget, z3cform_TextWidget):
pattern_options = BaseWidget.pattern_options.copy()
separator = ';'
- vocabulary = None
+ vocabulary = 'plone.app.vocabularies.Catalog'
vocabulary_view = '@@getVocabulary'
orderable = False
- def update(self, *args, **kwargs):
- value_type = getattr(self.field, 'value_type', None)
- if value_type:
- self.vocabulary = getattr(value_type,
- 'vocabularyName',
- 'plone.app.vocabularies.Catalog')
- if self.vocabulary is None:
- self.vocabulary = 'plone.app.vocabularies.Catalog'
- z3cform_TextWidget.update(self, *args, **kwargs)
-
def _base_args(self):
"""Method which will calculate _base class arguments.
@@ -664,17 +708,30 @@ def _base_args(self):
args['name'] = self.name
args['value'] = self.value
-
args.setdefault('pattern_options', {})
+
+ vocabulary_name = self.vocabulary
+ field = None
if IChoice.providedBy(self.field):
args['pattern_options']['maximumSelectionSize'] = 1
+ field = self.field
+ elif ICollection.providedBy(self.field):
+ field = self.field.value_type
+ if field is not None and field.vocabularyName:
+ vocabulary_name = field.vocabularyName
+
field_name = self.field and self.field.__name__ or None
args['pattern_options'] = dict_merge(
get_relateditems_options(self.context, args['value'],
- self.separator, self.vocabulary,
+ self.separator, vocabulary_name,
self.vocabulary_view, field_name),
args['pattern_options'])
+ if field and getattr(field, 'vocabulary', None):
+ form_url = self.request.getURL()
+ source_url = "%s/++widget++%s/@@getSource" % (form_url, self.name)
+ args['pattern_options']['vocabularyUrl'] = source_url
+
return args
@@ -770,9 +827,16 @@ def DatetimeFieldWidget(field, request):
@implementer(IFieldWidget)
-def RelatedItemsFieldWidget(field, request):
- # TODO: when field is type IRelationChoice configure widget to only allow
- # one item to be selected
+def AjaxSelectFieldWidget(field, request, extra=None):
+ if extra is not None:
+ request = extra
+ return FieldWidget(field, AjaxSelectWidget(request))
+
+
+ at implementer(IFieldWidget)
+def RelatedItemsFieldWidget(field, request, extra=None):
+ if extra is not None:
+ request = extra
return FieldWidget(field, RelatedItemsWidget(request))
diff --git a/plone/app/widgets/tests/test_browser.py b/plone/app/widgets/tests/test_browser.py
index 66ab761..6d31b2f 100644
--- a/plone/app/widgets/tests/test_browser.py
+++ b/plone/app/widgets/tests/test_browser.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
import os
+from mock import Mock
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import login
+from plone.app.testing import logout
from plone.app.testing import setRoles
from plone.app.widgets.browser import vocabulary
from plone.app.widgets.browser.file import FileUploadView
@@ -277,6 +279,122 @@ def testVocabularyUsers(self):
data = json.loads(view())
self.assertEqual(len(data['results']), amount)
+ def testSource(self):
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import Choice
+ from zope.schema.interfaces import ISource
+
+ @implementer(ISource)
+ class DummyCatalogSource(object):
+ def search_catalog(self, query):
+ querytext = query['SearchableText']['query']
+ return [Mock(id=querytext)]
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = Choice(source=DummyCatalogSource())
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ]
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ 'attributes': 'id',
+ })
+ data = json.loads(view())
+ self.assertEquals(len(data['results']), 1)
+ self.assertEquals(data['results'][0]['id'], 'foo')
+
+ def testSourceCollectionField(self):
+ # This test uses a collection field
+ # and a source providing the 'search' method
+ # to help achieve coverage.
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import List, Choice
+ from zope.schema.interfaces import ISource
+ from zope.schema.vocabulary import SimpleTerm
+
+ @implementer(ISource)
+ class DummySource(object):
+ def search(self, query):
+ terms = [SimpleTerm(query, query)]
+ return iter(terms)
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = List(value_type=Choice(source=DummySource()))
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ],
+ 'sort_on': 'id',
+ 'sort_order': 'ascending',
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ 'batch': json.dumps({'size': 10, 'page': 1}),
+ })
+ data = json.loads(view())
+ self.assertEquals(len(data['results']), 1)
+ self.assertEquals(data['results'][0]['id'], 'foo')
+
+ def testSourcePermissionDenied(self):
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import Choice
+ from zope.schema.interfaces import ISource
+
+ @implementer(ISource)
+ class DummyCatalogSource(object):
+ def search_catalog(self, query):
+ querytext = query['SearchableText']['query']
+ return [Mock(id=querytext)]
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = Choice(source=DummyCatalogSource())
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ]
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ })
+ logout()
+ data = json.loads(view())
+ self.assertEquals(data['error'], 'Vocabulary lookup not allowed.')
+
def testQueryStringConfiguration(self):
view = QueryStringIndexOptions(self.portal, self.request)
data = json.loads(view())
diff --git a/plone/app/widgets/tests/test_dx.py b/plone/app/widgets/tests/test_dx.py
index 7eba468..2ea1385 100644
--- a/plone/app/widgets/tests/test_dx.py
+++ b/plone/app/widgets/tests/test_dx.py
@@ -35,6 +35,7 @@
from zope.schema import Tuple
from zope.schema import Set
+import mock
import json
try:
@@ -170,6 +171,16 @@ def test_data_converter(self):
converter.toWidgetValue(date(21, 10, 30)),
)
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import DateWidget
+ from plone.app.widgets.dx import DateFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = DateFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, DateWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class DatetimeWidgetTests(unittest.TestCase):
@@ -305,6 +316,16 @@ def test_data_converter_timezone(self):
# cleanup
self.widget.context = None
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import DatetimeWidget
+ from plone.app.widgets.dx import DatetimeFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = DatetimeFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, DatetimeWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class SelectWidgetTests(unittest.TestCase):
@@ -396,8 +417,8 @@ def test_widget_list_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'orderable': True, 'multiple': True,
- 'separator': '.'},
+ 'pattern_options': {
+ 'orderable': True, 'multiple': True, 'separator': '.'},
'pattern': 'select2',
'value': (),
'items': [
@@ -421,8 +442,8 @@ def test_widget_tuple_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'orderable': True, 'multiple': True,
- 'separator': ';'},
+ 'pattern_options': {
+ 'orderable': True, 'multiple': True, 'separator': ';'},
'pattern': 'select2',
'value': (),
'items': [
@@ -447,7 +468,8 @@ def test_widget_set_not_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'multiple': True, 'separator': ';'},
+ 'pattern_options': {
+ 'multiple': True, 'separator': ';'},
'pattern': 'select2',
'value': (),
'items': [
@@ -660,6 +682,29 @@ def test_widget_set_not_orderable(self):
widget._base_args(),
)
+ def test_widget_choice(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from zope.schema.interfaces import ISource
+ widget = AjaxSelectWidget(self.request)
+ source = Mock()
+ alsoProvides(source, ISource)
+ widget.field = Choice(__name__='choicefield', source=source)
+ widget.name = 'choicefield'
+ self.assertEqual(
+ {
+ 'name': 'choicefield',
+ 'value': u'',
+ 'pattern': 'select2',
+ 'pattern_options': {
+ 'separator': ';',
+ 'maximumSelectionSize': 1,
+ 'vocabularyUrl':
+ 'http://127.0.0.1/++widget++choicefield/@@getSource',
+ },
+ },
+ widget._base_args(),
+ )
+
def test_widget_addform_url_on_addform(self):
from plone.app.widgets.dx import AjaxSelectWidget
widget = AjaxSelectWidget(self.request)
@@ -751,6 +796,27 @@ def test_data_converter_tuple(self):
'123;456;789',
)
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from plone.app.widgets.dx import AjaxSelectFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = AjaxSelectFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, AjaxSelectWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
+ def test_fieldwidget_sequence(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from plone.app.widgets.dx import AjaxSelectFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ vocabulary = Mock()
+ request = Mock()
+ widget = AjaxSelectFieldWidget(field, vocabulary, request)
+ self.assertTrue(isinstance(widget, AjaxSelectWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class QueryStringWidgetTests(unittest.TestCase):
@@ -837,19 +903,98 @@ def test_multiple_selection(self):
"""The pattern_options key maximumSelectionSize shouldn't be
set when the field allows multiple selections"""
from plone.app.widgets.dx import RelatedItemsFieldWidget
+ from zope.schema.interfaces import ISource
+ from zope.schema.vocabulary import VocabularyRegistry
+
context = Mock(absolute_url=lambda: 'fake_url')
context.portal_properties.site_properties\
.getProperty.return_value = ['SomeType']
field = List(
__name__='selectfield',
- value_type=Choice(values=['one', 'two', 'three'])
+ value_type=Choice(vocabulary='foobar')
)
widget = RelatedItemsFieldWidget(field, self.request)
widget.context = context
- widget.update()
- base_args = widget._base_args()
+
+ vocab = Mock()
+ alsoProvides(vocab, ISource)
+ with mock.patch.object(VocabularyRegistry, 'get', return_value=vocab):
+ widget.update()
+ base_args = widget._base_args()
patterns_options = base_args['pattern_options']
self.assertFalse('maximumSelectionSize' in patterns_options)
+ self.assertEqual(
+ patterns_options['vocabularyUrl'],
+ '/@@getVocabulary?name=foobar&field=selectfield',
+ )
+
+ def test_converter_RelationChoice(self):
+ from plone.app.widgets.dx import \
+ RelationChoiceRelatedItemsWidgetConverter
+ brain = Mock(getObject=Mock(return_value='obj'))
+ portal_catalog = Mock(return_value=[brain])
+ widget = Mock()
+ converter = RelationChoiceRelatedItemsWidgetConverter(
+ TextLine(), widget)
+
+ with mock.patch('plone.app.widgets.dx.IUUID', return_value='id'):
+ self.assertEqual(converter.toWidgetValue('obj'), 'id')
+ self.assertEqual(converter.toWidgetValue(None), None)
+
+ with mock.patch(
+ 'plone.app.widgets.dx.getToolByName',
+ return_value=portal_catalog):
+ self.assertEqual(converter.toFieldValue('id'), 'obj')
+ self.assertEqual(converter.toFieldValue(None), None)
+
+ def test_converter_RelationList(self):
+ from plone.app.widgets.dx import RelatedItemsDataConverter
+ from plone.app.widgets.dx import IRelationList
+ field = List()
+ alsoProvides(field, IRelationList)
+ brain1 = Mock(getObject=Mock(return_value='obj1'))
+ brain2 = Mock(getObject=Mock(return_value='obj2'))
+ portal_catalog = Mock(return_value=[brain1, brain2])
+ widget = Mock(separator=';')
+ converter = RelatedItemsDataConverter(field, widget)
+
+ self.assertEqual(converter.toWidgetValue(None), None)
+ with mock.patch(
+ 'plone.app.widgets.dx.IUUID', side_effect=['id1', 'id2']):
+ self.assertEqual(
+ converter.toWidgetValue(['obj1', 'obj2']), 'id1;id2')
+
+ self.assertEqual(converter.toFieldValue(None), None)
+ with mock.patch(
+ 'plone.app.widgets.dx.getToolByName',
+ return_value=portal_catalog):
+ self.assertEqual(
+ converter.toFieldValue('id1;id2'), ['obj1', 'obj2'])
+
+ def test_converter_List_of_Choice(self):
+ from plone.app.widgets.dx import RelatedItemsDataConverter
+ field = List()
+ widget = Mock(separator=';')
+ converter = RelatedItemsDataConverter(field, widget)
+
+ self.assertEqual(converter.toWidgetValue(None), None)
+ self.assertEqual(
+ converter.toWidgetValue(['id1', 'id2']), 'id1;id2')
+
+ self.assertEqual(converter.toFieldValue(None), None)
+ self.assertEqual(
+ converter.toFieldValue('id1;id2'), ['id1', 'id2'])
+
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import RelatedItemsWidget
+ from plone.app.widgets.dx import RelatedItemsFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ vocabulary = Mock()
+ request = Mock()
+ widget = RelatedItemsFieldWidget(field, vocabulary, request)
+ self.assertTrue(isinstance(widget, RelatedItemsWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
def add_mock_fti(portal):
Repository: plone.app.widgets
Branch: refs/heads/master
Date: 2014-03-08T13:52:42Z
Author: Rok Garbas (garbas) <rok at garbas.si>
Commit: https://github.com/plone/plone.app.widgets/commit/e9623d07e981f80265df5759e232127b6baabd80
Merge pull request #62 from plone/davisagli-ajax-sources
improvements to RelatedItemsWidget
Files changed:
M plone/app/widgets/browser/configure.zcml
M plone/app/widgets/browser/vocabulary.py
M plone/app/widgets/configure.zcml
M plone/app/widgets/dx.py
M plone/app/widgets/tests/test_browser.py
M plone/app/widgets/tests/test_dx.py
diff --git a/plone/app/widgets/browser/configure.zcml b/plone/app/widgets/browser/configure.zcml
index fb4fbd1..d681e53 100644
--- a/plone/app/widgets/browser/configure.zcml
+++ b/plone/app/widgets/browser/configure.zcml
@@ -14,6 +14,13 @@
/>
<browser:page
+ name="getSource"
+ for="z3c.form.interfaces.IWidget"
+ class=".vocabulary.SourceView"
+ permission="zope.Public"
+ />
+
+ <browser:page
name="fileUpload"
for="Products.CMFCore.interfaces._content.IFolderish"
class=".file.FileUploadView"
diff --git a/plone/app/widgets/browser/vocabulary.py b/plone/app/widgets/browser/vocabulary.py
index c83d82f..e1cc615 100644
--- a/plone/app/widgets/browser/vocabulary.py
+++ b/plone/app/widgets/browser/vocabulary.py
@@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager
+from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.Five import BrowserView
-from Products.ZCTextIndex.ParseTree import ParseError
from logging import getLogger
-from plone.app.vocabularies.interfaces import ISlicableVocabulary
+from plone.app.querystring import queryparser
from plone.app.widgets.interfaces import IFieldPermissionChecker
+from plone.autoform.interfaces import WRITE_PERMISSIONS_KEY
+from plone.supermodel.utils import mergedTaggedValueDict
from types import FunctionType
+from zope.component import getUtility
from zope.component import queryAdapter
from zope.component import queryUtility
+from zope.schema.interfaces import ICollection
from zope.schema.interfaces import IVocabularyFactory
-
+from zope.security.interfaces import IPermission
import inspect
+import itertools
import json
logger = getLogger(__name__)
@@ -39,14 +44,11 @@ def _parseJSON(s):
_safe_callable_metadata = ['getURL', 'getPath']
-class VocabularyView(BrowserView):
+class VocabLookupException(Exception):
+ pass
- def error(self):
- return json.dumps({
- 'results': [],
- 'total': 0,
- 'error': True
- })
+
+class BaseVocabularyView(BrowserView):
def __call__(self):
"""
@@ -66,89 +68,57 @@ def __call__(self):
size: size of paged results
}
"""
- context = self.context
+ context = self.get_context()
self.request.response.setHeader("Content-type", "application/json")
- factory_name = self.request.get('name', None)
- field_name = self.request.get('field', None)
- if not factory_name:
- return json.dumps({'error': 'No factory provided.'})
- authorized = None
- sm = getSecurityManager()
- if (factory_name not in _permissions or
- not IPloneSiteRoot.providedBy(context)):
- # Check field specific permission
- if field_name:
- permission_checker = queryAdapter(context,
- IFieldPermissionChecker)
- if permission_checker is not None:
- authorized = permission_checker.validate(field_name,
- factory_name)
- if not authorized:
- return json.dumps({'error': 'Vocabulary lookup not allowed'})
- # Short circuit if we are on the site root and permission is
- # in global registry
- elif not sm.checkPermission(_permissions[factory_name], context):
- return json.dumps({'error': 'Vocabulary lookup not allowed'})
-
- factory = queryUtility(IVocabularyFactory, factory_name)
- if not factory:
- return json.dumps({
- 'error': 'No factory with name "%s" exists.' % factory_name})
-
- # check if factory accepts query argument
- query = _parseJSON(self.request.get('query', ''))
- batch = _parseJSON(self.request.get('batch', ''))
-
- if type(factory) is FunctionType:
- factory_spec = inspect.getargspec(factory)
- else:
- factory_spec = inspect.getargspec(factory.__call__)
try:
- supports_batch = False
- vocabulary = None
- if query and 'query' in factory_spec.args:
- if 'batch' in factory_spec.args:
- vocabulary = factory(self.context,
- query=query, batch=batch)
- supports_batch = True
- else:
- vocabulary = factory(self.context, query=query)
- elif query:
- raise KeyError("The vocabulary factory %s does not support "
- "query arguments",
- factory)
-
- if batch and supports_batch:
- vocabulary = factory(context, query, batch)
- elif query:
- vocabulary = factory(context, query)
- else:
- vocabulary = factory(context)
+ vocabulary = self.get_vocabulary()
+ except VocabLookupException, e:
+ return json.dumps({'error': e.message})
- except (TypeError, ParseError):
- raise
- return self.error()
+ results_are_brains = False
+ if hasattr(vocabulary, 'search_catalog'):
+ query = self.parsed_query()
+ results = vocabulary.search_catalog(query)
+ results_are_brains = True
+ elif hasattr(vocabulary, 'search'):
+ try:
+ query = self.parsed_query()['SearchableText']['query']
+ except KeyError:
+ results = iter(vocabulary)
+ else:
+ results = vocabulary.search(query)
+ else:
+ results = vocabulary
try:
- total = len(vocabulary)
+ total = len(results)
except TypeError:
total = 0 # do not error if object does not support __len__
# we'll check again later if we can figure some size
# out
+
+ # get batch
+ batch = _parseJSON(self.request.get('batch', ''))
if batch and ('size' not in batch or 'page' not in batch):
batch = None # batching not providing correct options
- logger.error("A vocabulary request contained bad batch "
- "information. The batch information is ignored.")
- if batch and not supports_batch and \
- ISlicableVocabulary.providedBy(vocabulary):
+ if batch:
# must be slicable for batching support
page = int(batch['page'])
# page is being passed in is 1-based
- start = (max(page-1, 0)) * int(batch['size'])
+ start = (max(page - 1, 0)) * int(batch['size'])
end = start + int(batch['size'])
- vocabulary = vocabulary[start:end]
+ # Try __getitem__-based slice, then iterator slice.
+ # The iterator slice has to consume the iterator through
+ # to the desired slice, but that shouldn't be the end
+ # of the world because at some point the user will hopefully
+ # give up scrolling and search instead.
+ try:
+ results = results[start:end]
+ except TypeError:
+ results = itertools.islice(results, start, end)
+ # build result items
items = []
attributes = _parseJSON(self.request.get('attributes', ''))
@@ -156,8 +126,11 @@ def __call__(self):
attributes = attributes.split(',')
if attributes:
- base_path = '/'.join(context.getPhysicalPath())
- for vocab_item in vocabulary:
+ portal = getToolByName(context, 'portal_url').getPortalObject()
+ base_path = '/'.join(portal.getPhysicalPath())
+ for vocab_item in results:
+ if not results_are_brains:
+ vocab_item = vocab_item.value
item = {}
for attr in attributes:
key = attr
@@ -167,8 +140,7 @@ def __call__(self):
continue
if key == 'path':
attr = 'getPath'
- vocab_value = vocab_item.value
- val = getattr(vocab_value, attr, None)
+ val = getattr(vocab_item, attr, None)
if callable(val):
if attr in _safe_callable_metadata:
val = val()
@@ -179,7 +151,7 @@ def __call__(self):
item[key] = val
items.append(item)
else:
- for item in vocabulary:
+ for item in results:
items.append({'id': item.token, 'text': item.title})
if total == 0:
@@ -189,3 +161,97 @@ def __call__(self):
'results': items,
'total': total
})
+
+ def parsed_query(self, ):
+ query = _parseJSON(self.request.get('query', '')) or {}
+ if query:
+ parsed = queryparser.parseFormquery(
+ self.get_context(), query['criteria'])
+ if 'sort_on' in query:
+ parsed['sort_on'] = query['sort_on']
+ if 'sort_order' in query:
+ parsed['sort_order'] = str(query['sort_order'])
+ query = parsed
+ return query
+
+
+class VocabularyView(BaseVocabularyView):
+ """Queries a named vocabulary and returns JSON-formatted results."""
+
+ def get_context(self):
+ return self.context
+
+ def get_vocabulary(self):
+ # Look up named vocabulary and check permission.
+
+ context = self.context
+ factory_name = self.request.get('name', None)
+ field_name = self.request.get('field', None)
+ if not factory_name:
+ raise VocabLookupException('No factory provided.')
+ authorized = None
+ sm = getSecurityManager()
+ if (factory_name not in _permissions or
+ not IPloneSiteRoot.providedBy(context)):
+ # Check field specific permission
+ if field_name:
+ permission_checker = queryAdapter(context,
+ IFieldPermissionChecker)
+ if permission_checker is not None:
+ authorized = permission_checker.validate(field_name,
+ factory_name)
+ if not authorized:
+ raise VocabLookupException('Vocabulary lookup not allowed')
+ # Short circuit if we are on the site root and permission is
+ # in global registry
+ elif not sm.checkPermission(_permissions[factory_name], context):
+ raise VocabLookupException('Vocabulary lookup not allowed')
+
+ factory = queryUtility(IVocabularyFactory, factory_name)
+ if not factory:
+ raise VocabLookupException(
+ 'No factory with name "%s" exists.' % factory_name)
+
+ # This part is for backwards-compatibility with the first
+ # generation of vocabularies created for plone.app.widgets,
+ # which take the (unparsed) query as a parameter of the vocab
+ # factory rather than as a separate search method.
+ if type(factory) is FunctionType:
+ factory_spec = inspect.getargspec(factory)
+ else:
+ factory_spec = inspect.getargspec(factory.__call__)
+ query = _parseJSON(self.request.get('query', ''))
+ if query and 'query' in factory_spec.args:
+ vocabulary = factory(context, query=query)
+ else:
+ # This is what is reached for non-legacy vocabularies.
+ vocabulary = factory(context)
+
+ return vocabulary
+
+
+class SourceView(BaseVocabularyView):
+ """Queries a field's source and returns JSON-formatted results."""
+
+ def get_context(self):
+ return self.context.context
+
+ def get_vocabulary(self):
+ widget = self.context
+ field = widget.field.bind(widget.context)
+
+ # check field's write permission
+ info = mergedTaggedValueDict(field.interface, WRITE_PERMISSIONS_KEY)
+ permission_name = info.get(field.__name__, 'cmf.ModifyPortalContent')
+ permission = queryUtility(IPermission, name=permission_name)
+ if permission is None:
+ permission = getUtility(
+ IPermission, name='cmf.ModifyPortalContent')
+ if not getSecurityManager().checkPermission(
+ permission.title, self.get_context()):
+ raise VocabLookupException('Vocabulary lookup not allowed.')
+
+ if ICollection.providedBy(field):
+ return field.value_type.vocabulary
+ else:
+ return field.vocabulary
diff --git a/plone/app/widgets/configure.zcml b/plone/app/widgets/configure.zcml
index 5f5809c..4cf39ae 100644
--- a/plone/app/widgets/configure.zcml
+++ b/plone/app/widgets/configure.zcml
@@ -156,6 +156,7 @@
<adapter factory=".dx.SelectWidgetConverter" />
<adapter factory=".dx.AjaxSelectWidgetConverter" />
<adapter factory=".dx.QueryStringDataConverter" />
+ <adapter factory=".dx.RelationChoiceRelatedItemsWidgetConverter" />
<adapter factory=".dx.RelatedItemsDataConverter" />
</configure>
@@ -236,6 +237,12 @@
z3c.form.interfaces.IFormLayer"
/>
+ <adapter
+ factory=".dx.RelatedItemsFieldWidget"
+ for="zope.schema.interfaces.IChoice
+ plone.app.vocabularies.catalog.CatalogSource
+ z3c.form.interfaces.IFormLayer" />
+
<adapter factory=".dx.QueryStringFieldWidget" />
<adapter factory=".dx.RichTextFieldWidget" />
diff --git a/plone/app/widgets/dx.py b/plone/app/widgets/dx.py
index c3a3643..67aa472 100644
--- a/plone/app/widgets/dx.py
+++ b/plone/app/widgets/dx.py
@@ -53,6 +53,7 @@
from zope.interface import implementer
from zope.interface import implements
from zope.interface import implementsOnly
+from zope.interface import Interface
from zope.publisher.browser import TestRequest
from zope.schema.interfaces import IChoice
from zope.schema.interfaces import ICollection
@@ -73,6 +74,17 @@
HAS_PAC = False
+try:
+ from z3c.relationfield.interfaces import IRelationChoice
+ from z3c.relationfield.interfaces import IRelationList
+except ImportError: # pragma: no cover
+ class IRelationChoice(Interface):
+ pass
+
+ class IRelationList(Interface):
+ pass
+
+
class IDateField(IDate):
"""Marker interface for the DateField."""
@@ -232,7 +244,8 @@ def toFieldValue(self, value):
class AjaxSelectWidgetConverter(BaseDataConverter):
- """Data converter for ICollection."""
+ """Data converter for ICollection fields using the AjaxSelectWidget.
+ """
adapts(ICollection, IAjaxSelectWidget)
@@ -272,8 +285,34 @@ def toFieldValue(self, value):
for v in value.split(separator))
+class RelationChoiceRelatedItemsWidgetConverter(BaseDataConverter):
+ """Data converter for RelationChoice fields using the RelatedItemsWidget.
+ """
+
+ adapts(IRelationChoice, IRelatedItemsWidget)
+
+ def toWidgetValue(self, value):
+ if not value:
+ return self.field.missing_value
+ return IUUID(value)
+
+ def toFieldValue(self, value):
+ if not value:
+ return self.field.missing_value
+ try:
+ catalog = getToolByName(self.widget.context, 'portal_catalog')
+ except AttributeError:
+ catalog = getToolByName(getSite(), 'portal_catalog')
+
+ res = catalog(UID=value)
+ if res:
+ return res[0].getObject()
+ else:
+ return self.field.missing_value
+
+
class RelatedItemsDataConverter(BaseDataConverter):
- """Data converter for ICollection."""
+ """Data converter for ICollection fields using the RelatedItemsWidget."""
adapts(ICollection, IRelatedItemsWidget)
@@ -289,7 +328,10 @@ def toWidgetValue(self, value):
if not value:
return self.field.missing_value
separator = getattr(self.widget, 'separator', ';')
- return separator.join([IUUID(o) for o in value if value])
+ if IRelationList.providedBy(self.field):
+ return separator.join([IUUID(o) for o in value if value])
+ else:
+ return separator.join(v for v in value if v)
def toFieldValue(self, value):
"""Converts from widget value to field.
@@ -300,24 +342,26 @@ def toFieldValue(self, value):
:returns: List of content objects
:rtype: list | tuple | set
"""
+ if not value:
+ return self.field.missing_value
+
collectionType = self.field._type
if isinstance(collectionType, tuple):
collectionType = collectionType[-1]
- if not len(value):
- return self.field.missing_value
-
separator = getattr(self.widget, 'separator', ';')
value = value.split(separator)
- value = [v.split('/')[0] for v in value]
- try:
- catalog = getToolByName(self.widget.context, 'portal_catalog')
- except AttributeError:
- catalog = getToolByName(getSite(), 'portal_catalog')
+ if IRelationList.providedBy(self.field):
+ try:
+ catalog = getToolByName(self.widget.context, 'portal_catalog')
+ except AttributeError:
+ catalog = getToolByName(getSite(), 'portal_catalog')
- return collectionType(item.getObject()
- for item in catalog(UID=value) if item)
+ return collectionType(item.getObject()
+ for item in catalog(UID=value) if item)
+ else:
+ return collectionType(v for v in value)
class QueryStringDataConverter(BaseDataConverter):
@@ -578,11 +622,6 @@ class AjaxSelectWidget(BaseWidget, z3cform_TextWidget):
vocabulary_view = '@@getVocabulary'
orderable = False
- def update(self, *args, **kwargs):
- if not hasattr(self, 'vocabulary'):
- self.vocabulary = getattr(self.field, 'vocabularyName', None)
- z3cform_TextWidget.update(self, *args, **kwargs)
-
def _base_args(self):
"""Method which will calculate _base class arguments.
@@ -610,12 +649,27 @@ def _base_args(self):
if IAddForm.providedBy(getattr(self, 'form')):
context = self.form
+ vocabulary_name = self.vocabulary
+ field = None
+ if IChoice.providedBy(self.field):
+ args['pattern_options']['maximumSelectionSize'] = 1
+ field = self.field
+ elif ICollection.providedBy(self.field):
+ field = self.field.value_type
+ if not vocabulary_name and field is not None:
+ vocabulary_name = field.vocabularyName
+
args['pattern_options'] = dict_merge(
get_ajaxselect_options(context, args['value'], self.separator,
- self.vocabulary, self.vocabulary_view,
+ vocabulary_name, self.vocabulary_view,
field_name),
args['pattern_options'])
+ if field and getattr(field, 'vocabulary', None):
+ form_url = self.request.getURL()
+ source_url = "%s/++widget++%s/@@getSource" % (form_url, self.name)
+ args['pattern_options']['vocabularyUrl'] = source_url
+
# ISequence represents an orderable collection
if ISequence.providedBy(self.field) or self.orderable:
args['pattern_options']['orderable'] = True
@@ -634,20 +688,10 @@ class RelatedItemsWidget(BaseWidget, z3cform_TextWidget):
pattern_options = BaseWidget.pattern_options.copy()
separator = ';'
- vocabulary = None
+ vocabulary = 'plone.app.vocabularies.Catalog'
vocabulary_view = '@@getVocabulary'
orderable = False
- def update(self, *args, **kwargs):
- value_type = getattr(self.field, 'value_type', None)
- if value_type:
- self.vocabulary = getattr(value_type,
- 'vocabularyName',
- 'plone.app.vocabularies.Catalog')
- if self.vocabulary is None:
- self.vocabulary = 'plone.app.vocabularies.Catalog'
- z3cform_TextWidget.update(self, *args, **kwargs)
-
def _base_args(self):
"""Method which will calculate _base class arguments.
@@ -664,17 +708,30 @@ def _base_args(self):
args['name'] = self.name
args['value'] = self.value
-
args.setdefault('pattern_options', {})
+
+ vocabulary_name = self.vocabulary
+ field = None
if IChoice.providedBy(self.field):
args['pattern_options']['maximumSelectionSize'] = 1
+ field = self.field
+ elif ICollection.providedBy(self.field):
+ field = self.field.value_type
+ if field is not None and field.vocabularyName:
+ vocabulary_name = field.vocabularyName
+
field_name = self.field and self.field.__name__ or None
args['pattern_options'] = dict_merge(
get_relateditems_options(self.context, args['value'],
- self.separator, self.vocabulary,
+ self.separator, vocabulary_name,
self.vocabulary_view, field_name),
args['pattern_options'])
+ if field and getattr(field, 'vocabulary', None):
+ form_url = self.request.getURL()
+ source_url = "%s/++widget++%s/@@getSource" % (form_url, self.name)
+ args['pattern_options']['vocabularyUrl'] = source_url
+
return args
@@ -770,9 +827,16 @@ def DatetimeFieldWidget(field, request):
@implementer(IFieldWidget)
-def RelatedItemsFieldWidget(field, request):
- # TODO: when field is type IRelationChoice configure widget to only allow
- # one item to be selected
+def AjaxSelectFieldWidget(field, request, extra=None):
+ if extra is not None:
+ request = extra
+ return FieldWidget(field, AjaxSelectWidget(request))
+
+
+ at implementer(IFieldWidget)
+def RelatedItemsFieldWidget(field, request, extra=None):
+ if extra is not None:
+ request = extra
return FieldWidget(field, RelatedItemsWidget(request))
diff --git a/plone/app/widgets/tests/test_browser.py b/plone/app/widgets/tests/test_browser.py
index 66ab761..6d31b2f 100644
--- a/plone/app/widgets/tests/test_browser.py
+++ b/plone/app/widgets/tests/test_browser.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
import os
+from mock import Mock
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import login
+from plone.app.testing import logout
from plone.app.testing import setRoles
from plone.app.widgets.browser import vocabulary
from plone.app.widgets.browser.file import FileUploadView
@@ -277,6 +279,122 @@ def testVocabularyUsers(self):
data = json.loads(view())
self.assertEqual(len(data['results']), amount)
+ def testSource(self):
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import Choice
+ from zope.schema.interfaces import ISource
+
+ @implementer(ISource)
+ class DummyCatalogSource(object):
+ def search_catalog(self, query):
+ querytext = query['SearchableText']['query']
+ return [Mock(id=querytext)]
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = Choice(source=DummyCatalogSource())
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ]
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ 'attributes': 'id',
+ })
+ data = json.loads(view())
+ self.assertEquals(len(data['results']), 1)
+ self.assertEquals(data['results'][0]['id'], 'foo')
+
+ def testSourceCollectionField(self):
+ # This test uses a collection field
+ # and a source providing the 'search' method
+ # to help achieve coverage.
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import List, Choice
+ from zope.schema.interfaces import ISource
+ from zope.schema.vocabulary import SimpleTerm
+
+ @implementer(ISource)
+ class DummySource(object):
+ def search(self, query):
+ terms = [SimpleTerm(query, query)]
+ return iter(terms)
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = List(value_type=Choice(source=DummySource()))
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ],
+ 'sort_on': 'id',
+ 'sort_order': 'ascending',
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ 'batch': json.dumps({'size': 10, 'page': 1}),
+ })
+ data = json.loads(view())
+ self.assertEquals(len(data['results']), 1)
+ self.assertEquals(data['results'][0]['id'], 'foo')
+
+ def testSourcePermissionDenied(self):
+ from z3c.form.browser.text import TextWidget
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.schema import Choice
+ from zope.schema.interfaces import ISource
+
+ @implementer(ISource)
+ class DummyCatalogSource(object):
+ def search_catalog(self, query):
+ querytext = query['SearchableText']['query']
+ return [Mock(id=querytext)]
+
+ widget = TextWidget(self.request)
+ widget.context = self.portal
+ widget.field = Choice(source=DummyCatalogSource())
+ widget.field.interface = Interface
+
+ from plone.app.widgets.browser.vocabulary import SourceView
+ view = SourceView(widget, self.request)
+ query = {
+ 'criteria': [
+ {
+ 'i': 'SearchableText',
+ 'o': 'plone.app.querystring.operation.string.is',
+ 'v': 'foo'
+ }
+ ]
+ }
+ self.request.form.update({
+ 'query': json.dumps(query),
+ })
+ logout()
+ data = json.loads(view())
+ self.assertEquals(data['error'], 'Vocabulary lookup not allowed.')
+
def testQueryStringConfiguration(self):
view = QueryStringIndexOptions(self.portal, self.request)
data = json.loads(view())
diff --git a/plone/app/widgets/tests/test_dx.py b/plone/app/widgets/tests/test_dx.py
index b11ff38..fd96fae 100644
--- a/plone/app/widgets/tests/test_dx.py
+++ b/plone/app/widgets/tests/test_dx.py
@@ -35,6 +35,7 @@
from zope.schema import Tuple
from zope.schema import Set
+import mock
import json
try:
@@ -170,6 +171,16 @@ def test_data_converter(self):
converter.toWidgetValue(date(21, 10, 30)),
)
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import DateWidget
+ from plone.app.widgets.dx import DateFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = DateFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, DateWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class DatetimeWidgetTests(unittest.TestCase):
@@ -305,6 +316,16 @@ def test_data_converter_timezone(self):
# cleanup
self.widget.context = None
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import DatetimeWidget
+ from plone.app.widgets.dx import DatetimeFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = DatetimeFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, DatetimeWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class SelectWidgetTests(unittest.TestCase):
@@ -396,8 +417,8 @@ def test_widget_list_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'orderable': True, 'multiple': True,
- 'separator': '.'},
+ 'pattern_options': {
+ 'orderable': True, 'multiple': True, 'separator': '.'},
'pattern': 'select2',
'value': (),
'items': [
@@ -421,8 +442,8 @@ def test_widget_tuple_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'orderable': True, 'multiple': True,
- 'separator': ';'},
+ 'pattern_options': {
+ 'orderable': True, 'multiple': True, 'separator': ';'},
'pattern': 'select2',
'value': (),
'items': [
@@ -447,7 +468,8 @@ def test_widget_set_not_orderable(self):
{
'multiple': True,
'name': None,
- 'pattern_options': {'multiple': True, 'separator': ';'},
+ 'pattern_options': {
+ 'multiple': True, 'separator': ';'},
'pattern': 'select2',
'value': (),
'items': [
@@ -660,6 +682,29 @@ def test_widget_set_not_orderable(self):
widget._base_args(),
)
+ def test_widget_choice(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from zope.schema.interfaces import ISource
+ widget = AjaxSelectWidget(self.request)
+ source = Mock()
+ alsoProvides(source, ISource)
+ widget.field = Choice(__name__='choicefield', source=source)
+ widget.name = 'choicefield'
+ self.assertEqual(
+ {
+ 'name': 'choicefield',
+ 'value': u'',
+ 'pattern': 'select2',
+ 'pattern_options': {
+ 'separator': ';',
+ 'maximumSelectionSize': 1,
+ 'vocabularyUrl':
+ 'http://127.0.0.1/++widget++choicefield/@@getSource',
+ },
+ },
+ widget._base_args(),
+ )
+
def test_widget_addform_url_on_addform(self):
from plone.app.widgets.dx import AjaxSelectWidget
widget = AjaxSelectWidget(self.request)
@@ -751,6 +796,27 @@ def test_data_converter_tuple(self):
'123;456;789',
)
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from plone.app.widgets.dx import AjaxSelectFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ request = Mock()
+ widget = AjaxSelectFieldWidget(field, request)
+ self.assertTrue(isinstance(widget, AjaxSelectWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
+ def test_fieldwidget_sequence(self):
+ from plone.app.widgets.dx import AjaxSelectWidget
+ from plone.app.widgets.dx import AjaxSelectFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ vocabulary = Mock()
+ request = Mock()
+ widget = AjaxSelectFieldWidget(field, vocabulary, request)
+ self.assertTrue(isinstance(widget, AjaxSelectWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
+
class QueryStringWidgetTests(unittest.TestCase):
@@ -840,19 +906,98 @@ def test_multiple_selection(self):
"""The pattern_options key maximumSelectionSize shouldn't be
set when the field allows multiple selections"""
from plone.app.widgets.dx import RelatedItemsFieldWidget
+ from zope.schema.interfaces import ISource
+ from zope.schema.vocabulary import VocabularyRegistry
+
context = Mock(absolute_url=lambda: 'fake_url')
context.portal_properties.site_properties\
.getProperty.return_value = ['SomeType']
field = List(
__name__='selectfield',
- value_type=Choice(values=['one', 'two', 'three'])
+ value_type=Choice(vocabulary='foobar')
)
widget = RelatedItemsFieldWidget(field, self.request)
widget.context = context
- widget.update()
- base_args = widget._base_args()
+
+ vocab = Mock()
+ alsoProvides(vocab, ISource)
+ with mock.patch.object(VocabularyRegistry, 'get', return_value=vocab):
+ widget.update()
+ base_args = widget._base_args()
patterns_options = base_args['pattern_options']
self.assertFalse('maximumSelectionSize' in patterns_options)
+ self.assertEqual(
+ patterns_options['vocabularyUrl'],
+ '/@@getVocabulary?name=foobar&field=selectfield',
+ )
+
+ def test_converter_RelationChoice(self):
+ from plone.app.widgets.dx import \
+ RelationChoiceRelatedItemsWidgetConverter
+ brain = Mock(getObject=Mock(return_value='obj'))
+ portal_catalog = Mock(return_value=[brain])
+ widget = Mock()
+ converter = RelationChoiceRelatedItemsWidgetConverter(
+ TextLine(), widget)
+
+ with mock.patch('plone.app.widgets.dx.IUUID', return_value='id'):
+ self.assertEqual(converter.toWidgetValue('obj'), 'id')
+ self.assertEqual(converter.toWidgetValue(None), None)
+
+ with mock.patch(
+ 'plone.app.widgets.dx.getToolByName',
+ return_value=portal_catalog):
+ self.assertEqual(converter.toFieldValue('id'), 'obj')
+ self.assertEqual(converter.toFieldValue(None), None)
+
+ def test_converter_RelationList(self):
+ from plone.app.widgets.dx import RelatedItemsDataConverter
+ from plone.app.widgets.dx import IRelationList
+ field = List()
+ alsoProvides(field, IRelationList)
+ brain1 = Mock(getObject=Mock(return_value='obj1'))
+ brain2 = Mock(getObject=Mock(return_value='obj2'))
+ portal_catalog = Mock(return_value=[brain1, brain2])
+ widget = Mock(separator=';')
+ converter = RelatedItemsDataConverter(field, widget)
+
+ self.assertEqual(converter.toWidgetValue(None), None)
+ with mock.patch(
+ 'plone.app.widgets.dx.IUUID', side_effect=['id1', 'id2']):
+ self.assertEqual(
+ converter.toWidgetValue(['obj1', 'obj2']), 'id1;id2')
+
+ self.assertEqual(converter.toFieldValue(None), None)
+ with mock.patch(
+ 'plone.app.widgets.dx.getToolByName',
+ return_value=portal_catalog):
+ self.assertEqual(
+ converter.toFieldValue('id1;id2'), ['obj1', 'obj2'])
+
+ def test_converter_List_of_Choice(self):
+ from plone.app.widgets.dx import RelatedItemsDataConverter
+ field = List()
+ widget = Mock(separator=';')
+ converter = RelatedItemsDataConverter(field, widget)
+
+ self.assertEqual(converter.toWidgetValue(None), None)
+ self.assertEqual(
+ converter.toWidgetValue(['id1', 'id2']), 'id1;id2')
+
+ self.assertEqual(converter.toFieldValue(None), None)
+ self.assertEqual(
+ converter.toFieldValue('id1;id2'), ['id1', 'id2'])
+
+ def test_fieldwidget(self):
+ from plone.app.widgets.dx import RelatedItemsWidget
+ from plone.app.widgets.dx import RelatedItemsFieldWidget
+ field = Mock(__name__='field', title=u'', required=True)
+ vocabulary = Mock()
+ request = Mock()
+ widget = RelatedItemsFieldWidget(field, vocabulary, request)
+ self.assertTrue(isinstance(widget, RelatedItemsWidget))
+ self.assertIs(widget.field, field)
+ self.assertIs(widget.request, request)
def add_mock_fti(portal):
-------------------------------------------------------------------------------
-------------- next part --------------
A non-text attachment was scrubbed...
Name: CHANGES.log
Type: application/octet-stream
Size: 75826 bytes
Desc: not available
URL: <http://lists.plone.org/pipermail/plone-testbot/attachments/20140308/baa69b7a/attachment-0002.obj>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: build.log
Type: application/octet-stream
Size: 806987 bytes
Desc: not available
URL: <http://lists.plone.org/pipermail/plone-testbot/attachments/20140308/baa69b7a/attachment-0003.obj>
More information about the Testbot
mailing list