Skip to content

Commit

Permalink
add support for querying sources to the ajax select and related items…
Browse files Browse the repository at this point in the history
… widgets
  • Loading branch information
davisagli committed Mar 7, 2014
1 parent 18913e4 commit f1ec347
Show file tree
Hide file tree
Showing 6 changed files with 531 additions and 124 deletions.
7 changes: 7 additions & 0 deletions plone/app/widgets/browser/configure.zcml
Expand Up @@ -13,6 +13,13 @@
permission="zope2.View"
/>

<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"
Expand Down
228 changes: 147 additions & 81 deletions 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__)
Expand All @@ -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):
"""
Expand All @@ -66,98 +68,69 @@ 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', ''))
if isinstance(attributes, basestring) and attributes:
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
Expand All @@ -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()
Expand All @@ -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:
Expand All @@ -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
7 changes: 7 additions & 0 deletions plone/app/widgets/configure.zcml
Expand Up @@ -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>

Expand Down Expand Up @@ -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" />

Expand Down

1 comment on commit f1ec347

@mister-roboto
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TESTS FAILED
Mr.roboto url : http://jenkins.plone.org/roboto/get_info?push=b91fd919365c4ab7a28e00a7a89bfbc6
plone-5.0-python-2.7 [FAILURE]

Please sign in to comment.