{ "info": { "author": "Ross Patterson", "author_email": "me@rpatterson.net", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Plone", "Framework :: Plone :: 4.2", "Framework :: Plone :: 4.3", "License :: OSI Approved", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2 :: Only", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": ".. -*-doctest-*-\n\n=======================\ncollective.formcriteria\n=======================\n\nThis package extends the Products.ATContentTypes.criteria types to\ncreate search forms. Specifically, any criterion fields which are\nselected in each criterion's \"Form Fields\" will be rendered on the\nsearch form. The values set on the criteria edit form become the\ndefault values on the search form. Search terms submitted through the\nsearch form supplement any criteria not on the search form. IOW,\ncriteria not appearing on the form become the base query built into\nthe search form.\n\n.. contents:: Table of Contents\n\nA new \"Search Form\" display layout is provided that renders the search\nform and the collection body text but no results. The search form on\nthis layout will display results using the layout specified in the\n\"Form Results Layout\" field of the collection's edit form.\n\nThe search form can also be rendered in a search form portlet based on\nplone.portlet.collection. The portlet will not render on the search\nform view or the criteria edit form but otherwise will render the\nsearch form for the designated collection according to the portlet\nsettings.\n\nThus the collection can use either the search form or a results\nlisting as the display layout. Users can initiate searches using\neither the form or the portlet. The portlet also reflects any\nsubmitted search terms and thus provides a convenient way for users to\nrefine their searches.\n\nMultiple sort criteria can also be added that will render user\nselectable sort links on the batch macro. See\ncollective/formcriteria/criteria/sort.rst for more details.\n\nA CSV export action is also provided which provides a link to users\nallowing them to download the collections results, subject to the\nuser's query in the CSV format. This allows collections to be used,\nfor example, in conjunction with spreadsheet software for ad-hoc\nreporting or limited export to other systems.\n\nA folder contents table display layout is also included. This layout\nis not yet fully functional but provides the basis for some very rich\nsite admin functionality.\n\nWARNING: Uninstall\n==================\n\nUninstalling this product after having added any collections or adding\ncriteria to any collections, even ones added before install, is\nuntested and may leave your collections broken.\n\nForm Criteria\n=============\n\nStart with some content for search results.\n\n >>> from plone.app import testing\n >>> from Products.CMFCore.utils import getToolByName\n >>> portal = layer['portal']\n >>> membership = getToolByName(portal, 'portal_membership')\n >>> folder = membership.getHomeFolder(testing.TEST_USER_ID)\n >>> testing.login(portal, testing.TEST_USER_NAME)\n >>> folder['bar-document-title']\n \n >>> folder['baz-event-title']\n \n\nOpen a browser and log in as a normal user.\n\n >>> from plone.testing import z2\n >>> browser = z2.Browser(layer['app'])\n >>> browser.handleErrors = False\n >>> browser.open(portal.absolute_url())\n >>> browser.getLink('Log in').click()\n >>> browser.getControl('Login Name').value = testing.TEST_USER_NAME\n >>> browser.getControl(\n ... 'Password').value = testing.TEST_USER_PASSWORD\n >>> browser.getControl('Log in').click()\n\nAdd and publish a collection.\n\n >>> browser.open(folder.absolute_url())\n >>> browser.getLink('Collection').click()\n >>> browser.getControl('Title').value = 'Foo Topic Title'\n >>> browser.getControl('Save').click()\n >>> print browser.contents\n <...\n ...Changes saved...\n >>> foo_topic = folder['foo-topic-title']\n\n >>> browser.getLink('Submit').click()\n >>> print browser.contents\n <...\n ...Item state changed...\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> layer['portal'].portal_workflow.doActionFor(foo_topic, 'publish')\n >>> testing.login(portal, testing.TEST_USER_NAME)\n\n >>> import transaction\n >>> transaction.commit()\n\nChange the display layout of the collection to the \"Search Form\".\n\n >>> browser.getLink('Search Form').click()\n >>> print browser.contents\n <...\n ...View changed...\n\nLogin as a user that can manage portlets.\n\n >>> owner_browser = z2.Browser(layer['app'])\n >>> owner_browser.handleErrors = False\n >>> owner_browser.open(portal.absolute_url())\n >>> owner_browser.getLink('Log in').click()\n >>> owner_browser.getControl(\n ... 'Login Name').value = testing.SITE_OWNER_NAME\n >>> owner_browser.getControl(\n ... 'Password').value = testing.TEST_USER_PASSWORD\n >>> owner_browser.getControl('Log in').click()\n\nAdd the search form portlet for this collection to the folder.\n\n >>> owner_browser.open(folder.absolute_url())\n >>> owner_browser.getLink('Manage portlets').click()\n >>> owner_browser.getControl(\n ... 'Search form portlet', index=1).selected = True\n >>> owner_browser.getForm(index=3).submit() # manually w/o JS\n >>> print owner_browser.contents\n <...\n ...Add Search Form Portlet...\n\n >>> header = owner_browser.getControl('Portlet header')\n >>> header.value = 'Foo Search Form Title'\n >>> foo_topic_path = '/'.join(\n ... ('',)+ foo_topic.getPhysicalPath()[\n ... len(portal.getPhysicalPath()):])\n >>> header.mech_form.new_control(\n ... type='checkbox', name=\"form.target_collection\",\n ... attrs=dict(checked='checked', value=foo_topic_path))\n >>> owner_browser.getControl('Save').click()\n >>> print owner_browser.contents\n <...\n ...Foo Search Form Title...\n\nGo to the collection's edit tab and set the \"Form Results Layout\"\nfield.\n\n >>> browser.getLink('Edit').click()\n >>> browser.getControl('Collection').selected = True\n >>> browser.getControl('Save').click()\n >>> print browser.contents\n <...\n ...Changes saved...\n\nGo to the \"Criteria\" tab and add a criterion for the workflow state\nthat won't appear on the form. Then set the query term to return only\npublished content.\n\n >>> browser.getLink('Criteria').click()\n >>> form = browser.getForm(name='criteria_select')\n >>> form.getControl('State').selected = True\n >>> form.getControl(\n ... 'Select values from list', index=1).selected = True\n >>> form.getControl('Add criteria').click()\n >>> print browser.contents\n <...\n ...State...\n ...Select values from list...\n\nSince the test browser doesn't have JavaScript, test the\ndiscrimination of criteria types by index manually.\n\n >>> foo_topic.allowedCriteriaForField('review_state')\n ['FormSelectionCriterion', 'FormCheckboxCriterion',\n 'FormPulldownCriterion', 'FormSimpleStringCriterion',\n 'FormListCriterion', 'FormCommaCriterion', 'FormSortCriterion',\n 'FormContextCriterion']\n >>> foo_topic.allowedCriteriaForField(\n ... 'review_state', display_list=True)\n \n\nSet the query term and save.\n\n >>> form = browser.getForm(action=\"criterion_edit_form\", index=0)\n >>> form.getControl('published').selected = True\n >>> form.getControl('Save').click()\n >>> print browser.contents\n <...\n ...Changes saved...\n\nOpen another browser as an anonymous user.\n\n >>> anon_browser = z2.Browser(layer['app'])\n >>> anon_browser.handleErrors = False\n\nBefore the topic has any form criteria, the search form is not\npresent.\n\n >>> anon_browser.open(foo_topic.absolute_url()+'/atct_topic_view')\n >>> anon_browser.getForm(name=\"formcriteria_search\")\n Traceback (most recent call last):\n LookupError\n\nAdd a simple string criterion for the SearchableText index on the\ncriteria tab.\n\n >>> form = browser.getForm(name='criteria_select')\n >>> form.getControl('Search Text').selected = True\n >>> form.getControl(name=\"criterion_type\").getControl(\n ... 'Text', index=1).selected = True\n >>> form.getControl('Add criteria').click()\n >>> print browser.contents\n <...\n ...Search Text...\n ...A simple string criterion...\n\nSelect the criterion's 'value' field as a form field so it will appear\non the search form.\n\n >>> browser.getControl(\n ... name='crit__SearchableText_FormSimpleStringCriterion'\n ... '_formFields:list').getControl('Value').selected = True\n\nSet a default search term.\n\n >>> browser.getControl(\n ... name=\"crit__SearchableText_FormSimpleStringCriterion\"\n ... \"_value\").value = 'bar'\n >>> browser.getControl(name=\"form.button.Save\").click()\n >>> print browser.contents\n <...\n ...Changes saved...\n\nIf no form value have been submitted, such as on a fresh load of the\ntopic view, the default term will be used in the query returning only\none of the content objects.\n\n >>> len(foo_topic.queryCatalog())\n 1\n\n >>> anon_browser.open(foo_topic.absolute_url()+'/atct_topic_view')\n >>> anon_browser.getLink('Bar Document Title')\n \n >>> anon_browser.getLink('Baz Event Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\nNow that a form criterion has been added, the search form is\nrendered.\n\n >>> anon_browser.open(foo_topic.absolute_url())\n >>> form = anon_browser.getForm(name=\"formcriteria_search\")\n >>> 'formcriteria-portlet.css' in anon_browser.contents\n True\n \nCriterion fields that haven't been selected in \"Form Fields\" don't\nappear on the search form.\n\n >>> form.getControl(\n ... name='form_crit__SearchableText_FormSimpleStringCriterion'\n ... '_formFields:list')\n Traceback (most recent call last):\n LookupError: name\n 'form_crit__SearchableText_FormSimpleStringCriterion_formFields:list'\n\nThe label for the criterion corresponds to the form element for the\nfirst criterion field.\n\n >>> ctl = form.getControl('Search Text')\n\nEnter a search term and submit the query. The topic will now list the\nother content object.\n\n >>> ctl.value = 'baz'\n >>> form.getControl(name='submit').click()\n >>> anon_browser.getLink('Bar Document Title')\n Traceback (most recent call last):\n LinkNotFoundError\n >>> anon_browser.getLink('Baz Event Title')\n \n\nSince the search form has been submitted, the results are rendered on\nthe layout specified by the \"Form Results Layout\".\n\n >>> anon_browser.url.startswith(\n ... 'http://nohost/plone/Members/test_user_1_/foo-topic-title'\n ... '/atct_topic_view')\n True\n\nThe search form portlet also reflects the search term submitted rather\nthan the default value submitted on the criteria tab.\n\n >>> form = anon_browser.getForm(name=\"formcriteria_search\")\n >>> ctl = form.getControl('Search Text')\n >>> ctl.value\n 'baz'\n\nIf the search form is submitted from this page, the results are still\nrendered on the same view.\n\n >>> ctl.value = 'bar'\n >>> form.getControl(name='submit').click()\n >>> anon_browser.url.startswith(\n ... 'http://nohost/plone/Members/test_user_1_/foo-topic-title'\n ... '/atct_topic_view')\n True\n\nValues are also ignored if submitted for criteria fields which are not\nlisted in \"Form Fields\".\n\n >>> crit = foo_topic.getCriterion(\n ... 'SearchableText_FormSimpleStringCriterion')\n >>> crit.setFormFields([])\n >>> transaction.commit()\n\n >>> anon_browser.open(\n ... foo_topic.absolute_url()+'/atct_topic_view'\n ... '?form_crit__SearchableText_FormSimpleStringCriterion'\n ... '_value=baz')\n >>> anon_browser.getLink('Bar Document Title')\n \n >>> anon_browser.getLink('Baz Event Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\n >>> crit.setFormFields(['value'])\n\n >>> import transaction\n >>> transaction.commit()\n\nThe search form handles index query parsing errors gracefully\ndisplaying a message to the user.\n\n >>> anon_browser.open(foo_topic.absolute_url())\n >>> form = anon_browser.getForm(name=\"formcriteria_search\")\n >>> ctl = form.getControl('Search Text')\n >>> ctl.value = 'bar (baz)'\n >>> form.getControl(name='submit').click()\n >>> print anon_browser.contents\n <...There are currently no results for this search...\n >>> anon_browser.getLink('Bar Document Title')\n Traceback (most recent call last):\n LinkNotFoundError\n >>> anon_browser.getLink('Baz Event Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\nThe search form portlet successfully renders when viewed on a context\nother than the portlet.\n\n >>> anon_browser.open(folder.absolute_url())\n >>> form = anon_browser.getForm(name=\"formcriteria_search\")\n\nEnsure that collective.formcriteria doesn't break existing ATTopic\ninstances such as those created by default in a Plone site.\n\n >>> owner_browser.open(portal.news.absolute_url())\n >>> print owner_browser.contents\n <...\n ...Site News...\n ...There are currently no items in this folder...\n\n >>> owner_browser.getLink('Edit').click()\n >>> print owner_browser.contents\n <...\n ... Search terms ...\n\nMake sure none of the collective.formcriteria extensions interfere\nwith existing ATTopic instances.\n\n >>> browser.open(portal.events.aggregator.absolute_url())\n\nAll criteria can also be created using poral_types.constructContent.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> foo_topic.deleteCriterion(\n ... 'crit__SearchableText_FormSimpleStringCriterion')\n >>> foo_topic.deleteCriterion(\n ... 'crit__review_state_FormSelectionCriterion')\n >>> seen = set()\n >>> topic_indexes = portal.portal_atct.topic_indexes\n >>> for field, index in sorted(topic_indexes.iteritems()):\n ... for criterion in index.criteria:\n ... if criterion in seen or criterion.startswith('AT'):\n ... continue\n ... portal.portal_types.constructContent(\n ... criterion, foo_topic,\n ... id='crit__%s_%s' % (field, criterion))\n ... seen.add(criterion)\n 'crit__Creator_FormSelectionCriterion'\n 'crit__Creator_FormCheckboxCriterion'\n 'crit__Creator_FormPulldownCriterion'\n 'crit__Creator_FormSimpleStringCriterion'\n 'crit__Creator_FormListCriterion'\n 'crit__Creator_FormCommaCriterion'\n 'crit__Creator_FormSortCriterion'\n 'crit__Creator_FormContextCriterion'\n 'crit__Date_FormDateCriterion'\n 'crit__Date_FormDateRangeCriterion'\n 'crit__Type_FormPortalTypeCriterion'\n 'crit__Type_FormPortalTypeCheckboxCriterion'\n 'crit__Type_FormPortalTypePulldownCriterion'...\n 'crit__path_FormPathCriterion'\n 'crit__path_FormRelativePathCriterion'\n\nInstalling\n==========\n\nThe 'default' profile is used when installing collective.formcriteria\nthrough the Plone Add-ons control panel\n\n >>> portal.portal_quickinstaller.uninstallProducts(['collective.formcriteria'])\n >>> print portal.portal_quickinstaller.installProducts(['collective.formcriteria'])\n Installed Products\n ====================\n collective.formcriteria:ok:\n >>> portal.portal_quickinstaller.getInstallProfiles(\n ... 'collective.formcriteria')[0]\n u'collective.formcriteria:default'\n\n.. -*-doctest-*-\n\nSorting\n=======\n\nTwo kinds of sort criteria are supported. Multiple fixed sort\ncriteria can be defined allowing the user to select from among them\nusing links on the batch macro. One form sort criterion can be added\nper collection to allows the user to specify a sort on the sort form.\nIf both are used, and the user has both submitted a sort from the form\nand selected a sort from the batch links, the latter criterion in the\nlist of criteria takes effect.\n\nForm sort criteria are not yet implemented.\n\nFixed Sort Criteria\n-------------------\n\nSet the item count to 1 so that batches will only have one item.\n\n >>> from Products.CMFCore.utils import getToolByName\n >>> portal = layer['portal']\n >>> membership = getToolByName(portal, 'portal_membership')\n\n >>> from plone.app import testing\n >>> folder = membership.getHomeFolder(testing.TEST_USER_ID)\n >>> foo_topic = folder['foo-topic-title']\n >>> foo_topic.setItemCount(1)\n\n >>> import transaction\n >>> transaction.commit()\n\nOpen a browser and log in as a normal user.\n\n >>> from plone.testing import z2\n >>> from plone.app import testing\n >>> portal = layer['portal']\n >>> browser = z2.Browser(layer['app'])\n >>> browser.handleErrors = False\n >>> browser.open(portal.absolute_url())\n >>> browser.getLink('Log in').click()\n >>> browser.getControl('Login Name').value = testing.TEST_USER_NAME\n >>> browser.getControl(\n ... 'Password').value = testing.TEST_USER_PASSWORD\n >>> browser.getControl('Log in').click()\n\nLoad the criteria edit form of a collection.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> browser.getLink('Criteria').click()\n\nThe sort selection form has been removed from the criteria tab.\n\n >>> browser.getForm(action=\"criterion_edit_form\", index=1)\n Traceback (most recent call last):\n IndexError: list index out of range\n\nInstead, multiple sort criteria can be added to a collection using the\nnormal criterion add form on the criteria tab.\n\n >>> form = browser.getForm(name=\"criteria_select\")\n >>> form.getControl('Relevance').selected = True\n >>> form.getControl('Sort results').selected = True\n >>> form.getControl('Add criteria').click()\n >>> print browser.contents\n <...\n ...Added criterion FormSortCriterion for field unsorted...\n\nAdd another sort criterion for the Date field reversed.\n\n >>> form = browser.getForm(name=\"criteria_select\")\n >>> form.getControl('Effective Date').selected = True\n >>> form.getControl('Sort results').selected = True\n >>> form.getControl('Add criteria').click()\n >>> print browser.contents\n <...\n ...Added criterion FormSortCriterion for field effective...\n\nChange the display layout of the collection to the \"Search Form\" then\nsubmit a search criteria to test that the sort links preserve search\ncriteria.\n\n >>> foo_topic.setLayout('criteria_form')\n >>> foo_topic.addCriterion(\n ... 'SearchableText','FormSimpleStringCriterion'\n ... ).setFormFields(['value'])\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.getLink('View').click()\n >>> form = browser.getForm(name=\"formcriteria_search\")\n >>> form.getControl('Search Text').value = 'blah'\n >>> form.getControl(name='submit').click()\n\nWhen the batch macro is rendered on a collection view, such as one of\nthe listings, it includes links to the different possible sorts in\norder. By default, the first sort criteria is selected. The sort\nlinks also have id's and CSS classes for styling support.\n\n >>> print browser.contents\n <...\n Sort on:\n Relevance\n Effective Date\n ...\n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(\n ... name=\"crit__unsorted_FormSortCriterion:boolean\")\n Traceback (most recent call last):\n LookupError: name\n 'crit__unsorted_FormSortCriterion:boolean'\n\nThe results are listed in order of weight.\n\n >>> browser.getLink('Baz Event Title')\n \n >>> browser.getLink('Bar Document Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\nWhen a sort link is clicked, that sort will show as selected and\nresults will be sorted according to the sort criteria.\n\n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(\n ... name=\"crit__effective_FormSortCriterion:boolean\").click()\n >>> print browser.contents\n <...\n ...Sort on:...\n ...Relevance...\n ...Effective Date...\n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(\n ... name=\"crit__effective_FormSortCriterion:boolean\")\n \n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(\n ... name=\"crit__unsorted_FormSortCriterion:boolean\")\n \n\nThe results reflect that the search query is preserved across the new\nsort selection.\n\n >>> browser.getLink('Bar Document Title')\n \n >>> browser.getLink('Baz Event Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\nIf the next batch is selected the sort and search query are\npreserved.\n\n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(name=\"b_start\", index=0).click()\n >>> browser.getLink('Bar Document Title')\n Traceback (most recent call last):\n LinkNotFoundError\n >>> browser.getLink('Baz Event Title')\n \n\nThe batch macro will render the sort links even if there's only one\nbatch.\n\n >>> foo_topic.setItemCount(0)\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url()+'/atct_topic_view')\n >>> form = browser.getForm(name=\"navigation_form\")\n >>> form.getControl(\n ... name=\"crit__effective_FormSortCriterion:boolean\")\n \n\nEnsure that the extended sort criteria work inside previously created\nATTopic instances.\n\n >>> topic = folder['at-topic-title']\n >>> topic.setSortCriterion('effective', True)\n >>> topic.queryCatalog()[0].getObject()\n \n\nGrouped Listing\n---------------\n\nA variation on the default collection view is provided that lists\nitems grouped by the sort used. This requires that the index used for\nsorting is also in the catalog metadata columns and this available on\nthe catalog brains.\n\nSort by creator to that we get at least one group with multiple\nitems.\n\n >>> foo_topic.deleteCriterion('crit__unsorted_FormSortCriterion')\n >>> foo_topic.setSortCriterion('Creator', False)\n\n >>> import transaction\n >>> transaction.commit()\n\nSelect the layout.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> browser.getLink('Grouped Listing').click()\n >>> print browser.contents\n <...\n ...View changed...\n >>> browser.getLink('Log out').click()\n\nNow the items are grouped by the sort values.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> print browser.contents\n <...\n ......\n ......\n ......\n\nThe grouped listing layout requires a sort criterion to render and\nraises an error if one is not present.\n\n >>> foo_topic.deleteCriterion('crit__Creator_ATSortCriterion')\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n Traceback (most recent call last):\n AssertionError: ...\n\nThe batch macros still work for topics that have no sort criteria.\n\n >>> foo_topic.setLayout('criteria_form')\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n >>> form = browser.getForm(name=\"formcriteria_search\")\n >>> form.getControl('Search Text').value = 'blah'\n >>> form.getControl(name='submit').click()\n >>> 'Sort on:' in browser.contents\n False\n\n.. -*-doctest-*-\n\nCSV Export\n==========\n\nThe data accessed in tabular form from collections is often exactly\nthe data site admins want to export into other formats such as CSV.\nThis package provides views for exporting the current query's\ncollection data into various formats. The CSV columns are taken from\nthe collections 'Table Columns' field on the edit tab/form regardless\nof whether the table layout is used. The CSV export link is available\nas a document action like the print and send-to actions.\n\nChange the columns and link columns.\n\n >>> from plone.testing import z2\n >>> from plone.app import testing\n >>> portal = layer['portal']\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n\n >>> from Products.CMFCore.utils import getToolByName\n >>> membership = getToolByName(portal, 'portal_membership')\n >>> folder = membership.getHomeFolder(testing.TEST_USER_ID)\n >>> foo_topic = folder['foo-topic-title']\n >>> columns = foo_topic.columns\n >>> columns.manage_delObjects(\n ... ['ModificationDate-column', 'get_size-column',\n ... 'review_state-column'])\n >>> columns['getPath-column'].update(filter='')\n >>> columns['Title-column'].update(link=False, sort='')\n >>> desc_column = columns[columns.invokeFactory(\n ... type_name='TopicColumn', id='Description-column',\n ... link=True)]\n >>> foo_topic.manage_delObjects(\n ... ['crit__sortable_title_FormSortCriterion',\n ... 'crit__get_size_FormSortCriterion',\n ... 'crit__modified_FormSortCriterion',\n ... 'crit__review_state_FormSortCriterion'])\n >>> testing.logout()\n\nAdd some criteria to the collection.\n\n >>> _ = foo_topic.addCriterion(\n ... 'path', 'FormRelativePathCriterion')\n >>> foo_topic.addCriterion(\n ... 'Type', 'FormSelectionCriterion'\n ... ).setValue(['Page', 'Event'])\n >>> foo_topic.getCriterion(\n ... 'SearchableText_FormSimpleStringCriterion'\n ... ).setFormFields(['value'])\n >>> _ = foo_topic.addCriterion(\n ... 'unsorted', 'FormSortCriterion')\n >>> _ = foo_topic.addCriterion(\n ... 'effective', 'FormSortCriterion')\n\n >>> import transaction\n >>> transaction.commit()\n\nOpen a browser and log in as a normal user.\n\n >>> browser = z2.Browser(layer['app'])\n >>> browser.handleErrors = False\n >>> browser.open(portal.absolute_url())\n >>> browser.getLink('Log in').click()\n >>> browser.getControl('Login Name').value = testing.TEST_USER_NAME\n >>> browser.getControl(\n ... 'Password').value = testing.TEST_USER_PASSWORD\n >>> browser.getControl('Log in').click()\n\nThe export link is now available. Download the raw, un-queried\ncollection results.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> export_link = browser.getLink('Export')\n >>> export_link.click()\n >>> browser.isHtml\n False\n >>> print browser.headers\n Status: 200 ...\n Content-Disposition: attachment...\n Content-Type: text/csv...\n\nSince the testbrowser can't handle file downloads, we'll check the CSV\noutput by calling the browser view directly.\n\n >>> from zope import interface\n >>> from collective.formcriteria import interfaces\n >>> interface.alsoProvides(layer['request'], interfaces.IFormCriteriaLayer)\n >>> from collective.formcriteria.testing import export\n\n >>> print export(portal, foo_topic, layer['request'], export_link.url)\n Status: 200 OK...\n Content-Type: text/csv\n Content-Disposition: attachment;filename=foo-topic-title.csv\n URL,Title,Description\n http://nohost/plone/Members/test_user_1_/foo-event-title,Foo Event Title,\n http://nohost/plone/Members/test_user_1_/bar-document-title,Bar Document Title,blah\n http://nohost/plone/Members/test_user_1_/baz-event-title,Baz Event Title,blah blah\n >>> testing.logout()\n\nAdd the search form portlet.\n\n >>> from zope import component\n >>> from plone.i18n.normalizer import (\n ... interfaces as normalizer_ifaces)\n >>> from collective.formcriteria.portlet import portlet\n >>> testing.login(portal, testing.TEST_USER_NAME)\n >>> manager = foo_topic.restrictedTraverse(\n ... '++contextportlets++plone.rightcolumn')\n >>> site_path_len = len(portal.getPhysicalPath())\n >>> assignment = portlet.Assignment(\n ... header='Foo Search Form Title',\n ... target_collection='/'.join(\n ... foo_topic.getPhysicalPath()[site_path_len:]))\n >>> name = component.getUtility(\n ... normalizer_ifaces.IIDNormalizer).normalize(\n ... assignment.title)\n >>> manager[name] = assignment\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\nSubmit a query. The exported CSV reflects the user submitted query\nand is sorted by relevance.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> form = browser.getForm(name=\"formcriteria_search\")\n >>> form.getControl('Search Text').value = 'blah'\n >>> form.getControl(name='submit').click()\n\n >>> export_link = browser.getLink('Export')\n >>> export_link.click()\n >>> browser.isHtml\n False\n >>> print browser.headers\n Status: 200...\n Content-Disposition: attachment;filename=foo-topic-title.csv\n Content-Length: ...\n Content-Type: text/csv...\n >>> print export(portal, foo_topic, layer['request'], export_link.url)\n URL,Title,Description\n http://nohost/plone/Members/test_user_1_/baz-event-title,Baz Event Title,blah blah\n http://nohost/plone/Members/test_user_1_/bar-document-title,Bar Document Title,blah\n\nSelect another sort, The exported CSV reflects the user selected sort\nand query.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> form = browser.getForm(name=\"formcriteria_search\")\n >>> form.getControl('Search Text').value = 'blah'\n >>> form.getControl(name='submit').click()\n >>> browser.getControl(\n ... name=\"crit__effective_FormSortCriterion:boolean\").click()\n\n >>> export_link = browser.getLink('Export')\n >>> export_link.click()\n >>> browser.isHtml\n False\n >>> print export(portal, foo_topic, layer['request'], export_link.url)\n URL,Title,Description\n http://nohost/plone/Members/test_user_1_/bar-document-title,Bar Document Title,blah\n http://nohost/plone/Members/test_user_1_/baz-event-title,Baz Event Title,blah blah\n\nIt is also possible to change the CSV format by passing in request\nkeys with a special 'csv.fmtparam-' prefix. These values are passed\ninto Python's csv.writer() factory as keyword arguments. For example,\nto use a tab character as a delimiter instead of \",\", add a\n'csv.fmtparam-delimiter' key to the request.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> export_url = browser.getLink('Export').url\n >>> browser.open(export_url + '&csv.fmtparam-delimiter=%09')\n >>> browser.isHtml\n False\n >>> print export(portal, foo_topic, layer['request'],\n ... export_url + '&csv.fmtparam-delimiter=%09')\n URL\tTitle\tDescription\n http://nohost/plone/Members/test_user_1_/foo-event-title\tFoo Event Title\t\n http://nohost/plone/Members/test_user_1_/bar-document-title\tBar Document Title\tblah\n http://nohost/plone/Members/test_user_1_/baz-event-title\tBaz Event Title\tblah blah\n\nIt's also possible to add a column for fields that don't have\ncorresponding catalog metadata. Be aware that using such columns can\ngreatly affect performance as export requires looking up every object\nto retrieve the data.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> text_column = columns[columns.invokeFactory(\n ... type_name='TopicColumn', id='getText-column',\n ... title='Text', link=True)]\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(export_url)\n >>> browser.isHtml\n False\n >>> print export(portal, foo_topic, layer['request'], export_url)\n URL,Title,Description,Text\n http://nohost/plone/Members/test_user_1_/foo-event-title,Foo Event Title,,

foo...

\n http://nohost/plone/Members/test_user_1_/bar-document-title,Bar Document Title,blah,

bar...

\n http://nohost/plone/Members/test_user_1_/baz-event-title,Baz Event Title,blah blah,\n\nThe export link isn't available if there are no collection columns.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> foo_topic.manage_delObjects(['columns'])\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n >>> browser.getLink('Export')\n Traceback (most recent call last):\n LinkNotFoundError\n\n.. -*-doctest-*-\n\nContents View\n=============\n\nA version of the folder_contents can be used with collections\nwhere the columns are those specified in the collection's \"Table\nColumns\" field. The buttons at the bottom of the folder contents view\nwill then be applied to the selected items.\n\nAny columns that are selected in the collection's \"Table\nColumns\" field that are also selected in the \"Table Column Links\"\nfield will be rendered as links. Note that it's possible to select a\nlink column that isn't a table column which will have no effect.\n\nAdd a simple string criterion for the SearchableText index on the\ncriteria tab. Set a default search term. Add a sort criteria for\nconsistent ordering.\n\n >>> from plone.app import testing\n >>> from Products.CMFCore.utils import getToolByName\n >>> portal = layer['portal']\n >>> membership = getToolByName(portal, 'portal_membership')\n >>> folder = membership.getHomeFolder(testing.TEST_USER_ID)\n >>> foo_topic = folder['foo-topic-title']\n >>> crit = foo_topic.getCriterion(\n ... 'SearchableText_FormSimpleStringCriterion')\n >>> crit.setValue('bar')\n >>> crit.setFormFields(['value'])\n >>> sort = foo_topic.addCriterion(\n ... 'getPhysicalPath', 'FormSortCriterion')\n\n >>> import transaction\n >>> transaction.commit()\n\nOpen a browser and log in as a user who can change the display layout\nfor the topic.\n\n >>> from plone.testing import z2\n >>> browser = z2.Browser(layer['app'])\n >>> browser.handleErrors = False\n >>> browser.open(portal.absolute_url())\n >>> browser.getLink('Log in').click()\n >>> browser.getControl('Login Name').value = testing.TEST_USER_NAME\n >>> browser.getControl(\n ... 'Password').value = testing.TEST_USER_PASSWORD\n >>> browser.getControl('Log in').click()\n\nChange the topic's display layout and the search form results layout\nto the contents view.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> browser.getLink('Tabular Form').click()\n >>> print browser.contents\n <...\n ...View changed...\n\nThe view renders the contents form with the default columns.\n\n >>> browser.getForm(name=\"folderContentsForm\")\n \n >>> print browser.contents\n <...\n ...Title...\n ...Size...\n ...Modification Date...\n ...State...\n\nThe order column is not included since order is determined by the\ncollection and is fixed.\n\n >>> 'Order' in browser.contents\n False\n\nThe topic contents are listed in the contents table form and the\ntitles are links to the item.\n\n >>> print browser.contents\n <...\n ...Bar Document Title...\n ...2.9 kB...\n ...Published...\n >>> from collective.formcriteria.testing import CONTENT_FIXTURE\n >>> now = CONTENT_FIXTURE.now\n >>> str(portal.restrictedTraverse('@@plone').toLocalizedTime(now)\n ... ) in browser.contents\n True\n\n >>> browser.getControl('Bar Document Title')\n \n >>> browser.getLink('Bar Document Title')\n \n\nThe first sort criterion is the default sort.\n\n >>> browser.getControl(name=\"sort_on\").value\n 'sortable_title'\n\nSelect different collection columns and which columns link to the\nresult item.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> columns = foo_topic.columns\n >>> columns.manage_delObjects(\n ... ['ModificationDate-column', 'get_size-column',\n ... 'review_state-column'])\n >>> columns['Title-column'].update(link=False)\n >>> desc_column = columns[columns.invokeFactory(\n ... type_name='TopicColumn', id='Description-column',\n ... link=True)]\n >>> effective_column = columns[columns.invokeFactory(\n ... type_name='TopicColumn', id='EffectiveDate-column',\n ... link=True)]\n >>> foo_topic.manage_delObjects(\n ... ['crit__get_size_FormSortCriterion',\n ... 'crit__get_size_FormSimpleIntCriterion',\n ... 'crit__modified_FormSortCriterion',\n ... 'crit__modified_FormDateCriterion',\n ... 'crit__review_state_FormSortCriterion',\n ... 'crit__review_state_FormSelectionCriterion'])\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\nThe view renders the contents form with the specified columns.\n\n >>> browser.open(foo_topic.absolute_url())\n >>> browser.getForm(name=\"folderContentsForm\")\n \n >>> print browser.contents\n <...\n ...Description...\n ...Effective Date...\n ...Title...\n >>> 'Size' in browser.contents\n False\n >>> 'Modification Date' in browser.contents\n False\n >>> ' State ' in browser.contents\n False\n\nThe topic contents are also listed with the specified columns.\n\n >>> print browser.contents\n <...\n ...Bar Document Title...\n ...blah...\n >>> '2.9 kB' in browser.contents\n False\n >>> now.ISO() in browser.contents\n False\n >>> 'Published' in browser.contents\n False\n\nThe link columns have also been changed.\n\n >>> browser.getControl('Bar Document Title')\n \n >>> browser.getLink('blah')\n \n >>> browser.getLink((now-2).ISO())\n \n >>> browser.getLink('Bar Document Title')\n Traceback (most recent call last):\n LinkNotFoundError\n\nThe item selection header row reflects the new number of columns.\n\n >>> print browser.contents\n <...\n ......\n ......\n\nThe KSS update table view also reflects the selected columns.\n\n >>> browser.open(\n ... foo_topic.absolute_url()+'/foldercontents_update_table')\n >>> print browser.contents\n <...\n ...Description...\n ...Effective Date...\n ...Title...\n >>> 'Size' in browser.contents\n False\n >>> 'Modification Date' in browser.contents\n False\n >>> ' State ' in browser.contents\n False\n\nQuery Criteria\n--------------\n\nIf the query criteria have been assigned to a specific column, the\nwill be rendered in the filter table header row. Otherwise they will\nbe rendered in the search form as usual.\n\nAdd the portlet.\n\n >>> from zope import component\n >>> from plone.i18n.normalizer import (\n ... interfaces as normalizer_ifaces)\n >>> from collective.formcriteria.portlet import portlet\n >>> testing.login(portal, testing.TEST_USER_NAME)\n >>> manager = foo_topic.restrictedTraverse(\n ... '++contextportlets++plone.rightcolumn')\n >>> site_path_len = len(portal.getPhysicalPath())\n >>> assignment = portlet.Assignment(\n ... header='Foo Search Form Title',\n ... target_collection='/'.join(\n ... foo_topic.getPhysicalPath()[site_path_len:]))\n >>> name = component.getUtility(\n ... normalizer_ifaces.IIDNormalizer).normalize(\n ... assignment.title)\n >>> manager[name] = assignment\n >>> testing.logout()\n\nIf query criteria are configured for the table columns, a filter table\nhead row will be rendered as a search form.\n\n >>> foo_topic.setFormLayout('folder_contents')\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n >>> contents_form = browser.getForm(name=\"folderContentsForm\")\n >>> contents_form.getControl(\n ... name='form_crit__SearchableText_FormSimpleStringCriterion'\n ... '_value', index=0)\n \n >>> contents_form.getControl(\n ... name='form_crit__Title_FormSimpleStringCriterion_value',\n ... index=0)\n \n >>> contents_form.getControl('Filter', index=0)\n \n\nSince all query criteria are used in the table columns, no portlet\nsearch form is rendered.\n\n >>> browser.getForm(name=\"formcriteria_search\")\n Traceback (most recent call last):\n LookupError\n\nThe contents view reflects user criteria submitted through the\ncontents form.\n\n >>> contents_form.getControl(\n ... name='form_crit__SearchableText_FormSimpleStringCriterion'\n ... '_value', index=0).value = 'baz'\n >>> contents_form.getControl('Filter', index=0).click()\n >>> browser.getControl('Bar Document Title')\n Traceback (most recent call last):\n LookupError: label 'Bar Document Title'\n >>> browser.getControl('Baz Event Title')\n \n\nThe filter collapsible doesn't collapse when clicking on the search\ntext box.\n\n >>> import re\n >>> regexp = re.compile('http://.*?collapsiblesections.css')\n >>> regexp.search(browser.contents).group()\n 'http://nohost/plone/portal_css/Plone%20Default/collapsiblesections.css'\n >>> browser.open(portal.absolute_url() + '/collapsiblesections.css')\n >>> print browser.contents\n /*...\n #foldercontents-getPath-filter .collapsibleHeader {\n ...\n\nThe search form is rendered if query criteria are present which are\nnot assigned to a column.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> columns['getPath-column'].update(filter='')\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n >>> portlet_form = browser.getForm(name=\"formcriteria_search\")\n\nThe contents view also reflects user criteria submitted through the\nportlet form.\n\n >>> portlet_form.getControl(\n ... name='form_crit__SearchableText_FormSimpleStringCriterion'\n ... '_value').value = 'baz'\n >>> portlet_form.getControl(name='submit').click()\n >>> browser.getControl('Bar Document Title')\n Traceback (most recent call last):\n LookupError: label 'Bar Document Title'\n >>> browser.getControl('Baz Event Title')\n \n\nIf no query criteria are configured, the filter table head row will\nnot be rendered.\n\n >>> z2.login(portal.getPhysicalRoot().acl_users, testing.SITE_OWNER_NAME)\n >>> columns['Title-column'].update(filter='')\n >>> testing.logout()\n\n >>> import transaction\n >>> transaction.commit()\n\n >>> browser.open(foo_topic.absolute_url())\n >>> print browser.contents\n <...\n \n  \n \n  \n Title\n  \n \n \n  \n Description\n  \n \n \n  \n Effective Date\n  \n \n \n >> print browser.contents\n <...\n \n \n \n \n \n ...\n \n \n \n ...\n\nChangelog\n=========\n\n2.1 (2016-08-05)\n----------------\n\n* Added travis build script\n [bogdangi]\n\n* Fix issues with title and description inside form view and form criteria edit\n [bogdangi]\n\n* Added further Plone 4.3 compatibility [miohtama]\n\n* Use existing i18n message ID in form. [icemac]\n\n2.0 - 2012-11-01\n----------------\n\n* Plone 4.2 and 4.3 compatibility.\n [rossp]\n\n* Use localized format for DateTime column values.\n [rossp]\n\n* Add missing form criteria support for the recurse field of path\n criteria.\n [rossp]\n\n* Restrict some of the search form portlet support to the left and\n right column portlet managers on normal plone views, and\n additionally to the 4 dashboard portlet managers on the dashboard\n view for performance reasons. This means if you want to use the\n search form portlet in a manager for which these registrations don't\n apply, you'll have to use your own registration like the\n registrations in\n collective/formcriteria/form/configure.zcml.\n [rossp]\n\n* Update chameleon compatibility to the new 2.x branch.\n [rossp]\n\n* Optimize and improve performancde.\n [rossp]\n\n* Various upstream template updates.\n [rossp, topherh]\n\n* Add remote URL support to folder_summary_view.\n [topherh]\n\n* Fix an issue with the permission protecting the Export action.\n [rossp]\n\n* Handle catalog index ParseErrors gracefully. Thanks to Larry\n Pitcher for the report.\n [rossp]\n\n2.0b2 - 2011-01-13\n------------------\n\n* Retrofit some Plone 3 compatibility. Plone 3 is un-supported in\n version 2+. The tests pass but the UI hasn't been checked so YMMV.\n [rossp]\n\n2.0b1 - 2011-01-11\n------------------\n\n* Added a rudimentary UI for managing collection columns.\n [rossp]\n\n2.0a4 - 2011-01-11\n------------------\n\n* Restore the workflow menu which was missing from the topic\n folder_contents view. Thanks to Larry Pitcher for the report.\n [rossp]\n\n2.0a3 - 2011-01-10\n------------------\n\n* Change the permission for the grouped listing view so that anonymous\n visitors can access the view when the content is published.\n Thanks to Larry Pitcher for the report.\n [rossp]\n\n* Add support for columns that don't have corresponding catalog\n metadata. Be aware that using such columns can greatly affect\n performance as export requires looking up every object to retrieve\n the data.\n [rossp]\n\n2.0a2 - 2010-11-23\n------------------\n\n* Fixed a problem with a faulty type assumption regarding\n DateTimeFields in the FormDateCriterion. The FormDateCriterion\n doesn't actually used a DateTimeField.\n [rossp]\n\n* Refactor the
and