Skip to content
Open
Changes from all commits
Commits
Show all changes
278 commits
Select commit Hold shift + click to select a range
c89fd1a
Issue #2114593 by drunken monkey: Added list of floats to test module.
drunken-monkey Oct 22, 2013
602f36e
Issue #2086783 by drunken monkey: Removed Views field handlers for "v…
drunken-monkey Oct 23, 2013
5361183
Issue #2113277 by moonray, drunken monkey: Fixed date facet count for…
Oct 23, 2013
dde60e9
Adapted CHANGELOG.txt to the 1.9 release.
drunken-monkey Oct 23, 2013
40f2ca0
Issue #2100191 by drunken monkey, Bojhan: Added an admin description …
drunken-monkey Oct 24, 2013
5bbf737
Issue #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not…
drunken-monkey Oct 28, 2013
1bd1469
Follow-up to #2110315 by drunken monkey: Fixed Views filter for non-F…
drunken-monkey Nov 1, 2013
c734ecd
Issue #1750144 by jsacksick, drunken monkey: Fixed missing Boost opti…
Nov 2, 2013
db31e0c
Issue #2115127 by drunken monkey: Fixed cron indexing logic to keep t…
drunken-monkey Nov 3, 2013
db1d84f
Issue #2100199 by drunken monkey: Merged index tabs for a cleaner look.
drunken-monkey Nov 6, 2013
a7882f5
Fixed one comment line length.
drunken-monkey Nov 7, 2013
b7795f2
Issue #2100193 by drunken monkey: Turned operations in overview into …
drunken-monkey Nov 7, 2013
725819d
Issue #2100231 by drunken monkey: Renamed "Workflow" tab to "Filters".
drunken-monkey Nov 7, 2013
8723c72
Issue #1961120 by drunken monkey: Fixed Views handling of short fullt…
drunken-monkey Nov 8, 2013
2a4b08d
Issue #2118589 by mxr576, drunken monkey: Added node access for comme…
Nov 8, 2013
4ec1344
Removed left-over CSS and applied some documentation fixes.
drunken-monkey Nov 11, 2013
cb74b84
Some doc style fixes.
drunken-monkey Nov 13, 2013
dd5c678
Issue #1832334 by Damien Tournoud, drunken monkey: Fixed performance …
Nov 13, 2013
3335094
Some doc comment fixes for includes/query.inc.
drunken-monkey Nov 13, 2013
5246b64
Issue #2135255 by dww: Fixed missing pager on first page of search re…
Nov 14, 2013
3c886fe
Issue #1390598 by Damien Tournoud, drunken monkey: Added the concept …
Nov 14, 2013
e456c9e
Some doc comment fixes.
drunken-monkey Nov 14, 2013
1a10772
Issue #2135363 by drumm, drunken monkey: Added support for Views' use…
drumm Nov 15, 2013
ffb1ae2
Issue #1551302 by drunken monkey: Fixed the server tasks system.
drunken-monkey Nov 15, 2013
6158ba1
Issue #2128529 by Frando, drunken monkey: Added a way for facet query…
Nov 15, 2013
3ff385f
Issue #2128947 by stBorchert, drunken monkey: Fixed facet handling fo…
Nov 15, 2013
643d50b
Issue #2128001 by drunken monkey: Fixed the logic of the "contains no…
drunken-monkey Nov 15, 2013
35c829f
Minimal doc comment fix.
drunken-monkey Nov 15, 2013
a9295a1
Some documentation and other comment fixes.
drunken-monkey Nov 19, 2013
9dd2634
Follow-up to #2135363 by drunken monkey: Fixed logic of the "skip res…
drunken-monkey Nov 19, 2013
d4a12a9
Issue #2136019 by drunken monkey: Fixed mapping callback for taxonomy…
drunken-monkey Nov 25, 2013
b7e8b15
Issue #2134509 by kscheirer, drunken monkey: Removed unused variables…
Nov 26, 2013
4541d6b
Issue #2143659 by khiminrm: Fixed typo in update function 7116.
Nov 26, 2013
ce8a47f
Issue #2139215 by drunken monkey: Fixed $context parameter of batch c…
drunken-monkey Nov 27, 2013
b91d5f5
Doc comment fixes for the implemented entity CRUD hooks.
drunken-monkey Nov 30, 2013
5cf7cc5
Issue #1925114 by azinck: Fixed Views Facet Block integration with Pa…
Dec 4, 2013
328d4c6
Issue #2139239 by drunken monkey: Fixed highlighting for the last wor…
drunken-monkey Dec 5, 2013
88c559f
Issue #2100671 by drunken monkey: Fixed stopwords processor to ignore…
drunken-monkey Dec 9, 2013
7167bdc
Issue #2144531 by drunken monkey: Fixed cloning of queries to clone f…
drunken-monkey Dec 9, 2013
456dd8d
Issue #2152327 by sirtet, miro_dietiker: Fixed typo in help text for …
Dec 9, 2013
3007d21
Issue #2130819 by drunken monkey, Bojhan: Added UI improvements for t…
drunken-monkey Dec 9, 2013
4bc2ea2
Adapted CHANGELOG.txt to 1.10 release.
drunken-monkey Dec 9, 2013
8a96aae
Added dev release to CHANGELOG.txt again.
drunken-monkey Dec 9, 2013
38ac5e5
Follow-up to #2130819 by drunken monkey: Fixed service class descript…
drunken-monkey Dec 11, 2013
0773327
Issue #2150347 by drunken monkey: Added access callbacks for indexes …
drunken-monkey Dec 13, 2013
e1d1026
Issue #2146435 by timkang: Fixed Views paging with custom pager add-ons.
Dec 13, 2013
201bd98
Issue #2156021 by jgullstr: Fixed confirm message when disabling serv…
Dec 13, 2013
c50aad5
Issue #2158873 by drumm, drunken monkey: Fixed "all of" operator of V…
drumm Dec 18, 2013
10fc54c
Issue #2155721 by rjacobs, drunken monkey: Added support for Views' g…
ryan-jacobs Dec 24, 2013
4ab448a
Issue #2159011 by idebr, drunken monkey: Fixed highlighting of keywor…
Dec 24, 2013
821537d
Follow-up to #2118589 by drunken monkey: Fixed false error messages w…
drunken-monkey Dec 24, 2013
120a45c
Issue #2155575 by drunken monkey: Fixed incorrect "Server index statu…
drunken-monkey Dec 25, 2013
37a848b
Issue #2155127 by drunken monkey: Clarified the scope of the "Node ac…
drunken-monkey Dec 25, 2013
4bf28b8
Issue #1879196 by drunken monkey: Fixed invalid old indexes causing e…
drunken-monkey Dec 25, 2013
9d3ad5f
Adapted CHANGELOG.txt to 1.11 release.
drunken-monkey Dec 25, 2013
c4114de
Back to dev version.
drunken-monkey Dec 25, 2013
08a1682
Issue #1227702 by drunken monkey: Improved error handling.
drunken-monkey Dec 25, 2013
fcb6474
Issue #2150779 by hefox: Fixed "Overridden" detection for index featu…
drunken-monkey Dec 25, 2013
7b9a3c7
Issue #2168713 by idebr: Fixed highlighting of keys containing slashes.
Feb 25, 2014
6cc53e4
Fixed small comment typo.
drunken-monkey Mar 7, 2014
3959b06
Follow-up to #2168713 by drunken monkey: Fixed highlighting of keys c…
drunken-monkey Mar 10, 2014
a001ef5
Issue #2198261 by drunken monkey: Fixed fatal error on view editing.
drunken-monkey Mar 11, 2014
a12513e
Corrected a tiny comment typo.
drunken-monkey Mar 17, 2014
56d9181
Issue #2187487 by drunken monkey: Fixed admin summary of language fil…
drunken-monkey Mar 19, 2014
67c8e21
Issue #1888174 by drunken monkey, ipallian: Fixed problems with date …
drunken-monkey Apr 13, 2014
73741e4
Issue #2219563 by drunken monkey: Added __toString() methods for quer…
drunken-monkey Apr 14, 2014
e424e73
Issue #2169455 by drunken monkey: Fixed "undefined index" in search_a…
drunken-monkey May 12, 2014
1735020
Issue #2195469 by freakalis, drunken monkey: Added "Exclude fields" o…
May 12, 2014
5092cf1
Issue #2198791 by drunken monkey: Fixed empty Views entity filters.
drunken-monkey May 12, 2014
0325f59
Issue pagination
drunken-monkey May 12, 2014
67bc21a
Issue #2179755 by drunken monkey, fago: Fixed whitespaces after HTML …
drunken-monkey May 12, 2014
ca46ff9
Issue #2135697 by drunken monkey: Fixed handling of HTML attributes i…
drunken-monkey May 12, 2014
bfbe1ac
Issue #2219553 by drunken monkey: Fixed Views fulltext filter operators.
drunken-monkey May 12, 2014
0859f9d
Issue #2233749 by rjacobs, drunken monkey: Added drush support to cha…
ryan-jacobs May 14, 2014
22a52ab
Issue #2256891 by justanothermark: Fixed "0" entity labels.
May 15, 2014
950e379
Issue #2265349 by drunken monkey: Marked _search_api_settings_equals(…
drunken-monkey May 23, 2014
43cca33
Adapted CHANGELOG.txt to 1.12 release.
drunken-monkey May 23, 2014
99b5e4b
Back to dev version.
drunken-monkey May 23, 2014
52b91fe
Issue #2204847 by drunken monkey, alanmackenzie: Fixed Views caching …
drunken-monkey May 23, 2014
c3206ec
Issue #2216345 by bacardi55, fabianderijk, drunken monkey: Fixed arra…
Jun 18, 2014
30d12b9
Issue #2272983 by idflood, drunken monkey: Fixed Highlighting process…
Jun 22, 2014
883af65
Issue #2278791 by drunken monkey | tksmd: Fixed excerpt when searchin…
drunken-monkey Jul 9, 2014
38ecbff
Issue #2146435 by alanmackenzie: Fixed Views paging with custom pager…
Jul 9, 2014
f87546a
Fixed signature of the SearchApiViewsQuery::getOption() proxy method.
drunken-monkey Jul 10, 2014
c7f2f3e
Issue #2145547 by aaronbauman: Fixed duplicated sorts (one exposed) i…
Jul 20, 2014
8f2d3a1
Issue #2281535 by areynolds, nicola85: Adapted to latest changes in V…
Jul 21, 2014
717d2fb
Adapted CHANGELOG.txt to 1.13 release.
drunken-monkey Jul 23, 2014
c705d48
Back to dev version.
drunken-monkey Jul 23, 2014
b283150
Issue #2278737 by drunken monkey: Fixed use of multiple Views fulltex…
drunken-monkey Aug 19, 2014
2feb041
Issue #2319263 by solotandem: Added easier way to subclass entity cla…
Aug 27, 2014
cdbd157
Follow-up to #2110315 by ufku, drunken monkey: Fixed Views field hand…
Aug 28, 2014
0483636
Issue #2305627 by drunken monkey, cpliakas: Fixed date facets not dis…
drunken-monkey Sep 8, 2014
ccc5ad7
Issue #1372092 by drunken monkey: Added an error message when no serv…
drunken-monkey Sep 8, 2014
3271ed0
Issue #2334727 by Alex Bukach, drunken monkey: Fixed Views caching do…
Sep 15, 2014
74d16a8
Issue #2305755 by drunken monkey, pfrenssen: Fixed invalidation of th…
drunken-monkey Sep 17, 2014
157758d
Fixed error message in search_api_search_api_query_alter().
drunken-monkey Sep 22, 2014
fc4c7f5
Issue #2174163 by drunken monkey: Fixed detection of field type chang…
drunken-monkey Oct 13, 2014
519172a
Issue #1184610 by drunken monkey: Added option to limit indexes to sp…
drunken-monkey Oct 30, 2014
88f06a8
Issue #2364875 by Xano: Fixed Views argument handler for fulltext fie…
Nov 12, 2014
926c2a0
Issue #2364247 by drunken monkey: Fixed documentation for SearchApiQu…
drunken-monkey Nov 13, 2014
20ed1e4
Issue #2359201 by drunken monkey: Added a "List" option to "Aggregate…
drunken-monkey Nov 13, 2014
86eafb0
Issue #2347367 by drunken monkey, das-peter: Fixed forgotten usages o…
drunken-monkey Nov 13, 2014
55d0a03
Issue #1861134 by Cyberwolf, jackbravo, drunken monkey: Fixed indexin…
Nov 23, 2014
c058c7b
Issue #2375447 by drunken monkey: Added clarifying comment to foreach…
drunken-monkey Nov 25, 2014
b8755fe
Little doc cleanup.
drunken-monkey Dec 11, 2014
03af04a
Issue #2371099 by drunken monkey: Fixed display of active "Exclude" f…
drunken-monkey Dec 15, 2014
a20d6c7
Issue #2382385 by illusionuk, drunken monkey: Fixed error handling wh…
Dec 16, 2014
848b9c0
Revert "Issue #1184610 by drunken monkey: Added option to limit index…
drunken-monkey Dec 26, 2014
968ec5a
Adapted CHANGELOG.txt to 1.14 release.
drunken-monkey Dec 26, 2014
c5f7135
Back to dev version.
drunken-monkey Dec 26, 2014
8e468fd
Issue #2387161 by drunken monkey: Added a hook for altering search re…
drunken-monkey Jan 27, 2015
ad51dda
Issue #2414367 by Darren Oh, drunken monkey: Fixed detection of missi…
Jan 27, 2015
fe7232b
Issue #2412895 by drunken monkey: Fixed entity load for Views entity …
drunken-monkey Mar 3, 2015
fee782c
Issue #1396222 by drunken monkey: Added a "First letter" aggregation …
drunken-monkey Mar 5, 2015
b09c1b5
Issue #1184610 by drunken monkey: Added option to limit indexes to sp…
drunken-monkey Apr 3, 2015
9f1f1cc
Issue #2450227 by StryKaizer, drunken monkey: Fixed OR facets on taxo…
Apr 3, 2015
3d21501
Issue #2450333 by drunken monkey: Added performance improvement when …
drunken-monkey May 23, 2015
5e330c1
Issue #2414425 by Darren Oh, drunken monkey: Fixed backend form valid…
May 23, 2015
b151a4a
Issue #2448849 by cgoffin: Added "year range" option for date filters.
May 23, 2015
21e7d62
Follow-up to #2450333 by drunken monkey: Fixed indexing of entity-val…
drunken-monkey May 27, 2015
21566e5
Issue #2190627 by m1n0, drunken monkey: Fixed fatal errors for views …
Jun 1, 2015
90ba4e2
Adapted CHANGELOG.txt to 1.15 release.
drunken-monkey Jun 3, 2015
8dbd951
Back to dev version.
drunken-monkey Jun 3, 2015
d8dab1a
Issue #2447213 by drunken monkey: Fixed issues with stale field setti…
drunken-monkey Jun 8, 2015
5990977
Fixed a small coding mistake.
drunken-monkey Jun 8, 2015
8dc293b
Issue #2489882 by dww: Fixed Views taxonomy filter with "is (not) emp…
Jun 30, 2015
834d1d1
Issue #2520684 by drunken monkey: Fixed "bundles" setting on indexes …
drunken-monkey Jul 16, 2015
eb4924d
Small code style fix.
drunken-monkey Aug 5, 2015
2ac29dc
Issue #2479453 by prics, drunken monkey: Added a Drush command to lis…
Aug 6, 2015
0cc0f80
Issue #2533096 by drunken monkey: Fixed uncaught exception when delet…
drunken-monkey Aug 7, 2015
f062a8b
Issue #2520934 by drunken monkey: Added an item type for indexing sev…
drunken-monkey Aug 7, 2015
fa3992d
Issue #1197538 by thePanz, k4v, drunken monkey, ayalon, nadavoid, tim…
Aug 27, 2015
03ea0c0
Issue #2491175 by ptmkenny, drunken monkey: Added a data alteration f…
Aug 29, 2015
8c217b1
Issue #2502819: Fixed example code for hook_search_api_query_alter().
drunken-monkey Aug 29, 2015
b85d9b9
Adapted CHANGELOG.txt to 1.16 release.
drunken-monkey Aug 30, 2015
e5a8012
Back to dev version.
drunken-monkey Aug 30, 2015
1f03fe1
Issue #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds.
Sep 6, 2015
11dcade
Issue #2524314 by drunken monkey: Fixed bundle-setting for taxonomy t…
drunken-monkey Sep 23, 2015
cd30e75
Issue #2565005 by drunken monkey: Properly escape labels of "checkbox…
drunken-monkey Oct 6, 2015
8f30ddb
Fixed the docs for SearchApiQueryInterface::execute().
drunken-monkey Oct 15, 2015
7c30455
Issue #2567775 by joseph.olstad, drunken monkey: Fixed handling of br…
Oct 22, 2015
a5d4bf7
Issue #2563793 by drunken monkey, smitty, ReBa: Fixed Views base tabl…
drunken-monkey Nov 3, 2015
2df931c
Issue #2565743 by drunken monkey: Fixed creation of comment indexes w…
drunken-monkey Nov 3, 2015
20f3662
Fixed the CHANGELOG.txt format.
drunken-monkey Nov 9, 2015
0edea1d
Issue #1956650 by drunken monkey, wwhurley: Fixed trackItemChange not…
drunken-monkey Nov 10, 2015
ed93634
Issue #2570879 by thePanz, drunken monkey: Added sorting of remembere…
Nov 17, 2015
4c40fae
Issue #2592231 by drunken monkey, balintcsaba: Fixed ignored item lan…
drunken-monkey Nov 28, 2015
60008e6
Issue #2583263 by drunken monkey: Fixed Views integration in combinat…
drunken-monkey Dec 7, 2015
18f5d1a
Issue #2529262 by kingmackenzie, stefan.r: Added an option to Views d…
Dec 7, 2015
2bc929e
Small code formatting change.
drunken-monkey Dec 7, 2015
d0cedab
Issue #2603500 by drunken monkey, krishna savithraj: Fixed Views full…
drunken-monkey Dec 8, 2015
ae85052
Issue #2611726 by Hubbs, rakesh.gectcr: Fixed several typos.
Dec 9, 2015
2adee7b
Issue #2613054 by temkin: Fixed the "search-api-index" Drush command …
Dec 9, 2015
3547208
Fixed some minor typos in doc comments.
drunken-monkey Dec 10, 2015
0c0083d
Issue #2611714 by rakesh.gectcr, drunken monkey: Improved compliance …
Dec 11, 2015
94ad069
Issue #2572487 by drunken monkey: Removed operator setting for date f…
drunken-monkey Dec 12, 2015
4c94773
Issue #2576265 by drunken monkey: Fixed view trying to search on non-…
drunken-monkey Dec 12, 2015
52d9bb5
Issue #2631276 by tauno: Fixed the MLT handler for multi-entity indexes.
Jan 9, 2016
8da4346
Issue #2569461 by kraynuk.m, drunken monkey: Fixed existing table in …
Jan 9, 2016
9a97857
Issue #2629136 by drunken monkey, deranga: Fixed wrong facet counts i…
drunken-monkey Feb 10, 2016
39e9e86
Issue #2638740 by joachim, drunken monkey: Added a link to the index …
joachim-n Feb 15, 2016
9fa6ac3
Issue #2639200 by joachim: Added sorting to "related fields" select box.
joachim-n Feb 15, 2016
4297217
Issue #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">…
drunken-monkey Feb 23, 2016
db053d9
Issue #2667872 by Les Lim: Added "0" to field boost options.
Feb 26, 2016
d88b02d
Issue #2678856 by stefan.r, drunken monkey: Fixed date facets showing…
stefanruijsenaars Mar 14, 2016
ea7e6f6
Issue #2677900 by stefan.r, drunken monkey: Added the possibility to …
stefanruijsenaars Mar 14, 2016
8b1bcf1
Issue #2665586 by recrit, drunken monkey: Fixed parsing of invalid da…
Mar 14, 2016
1663438
Adapted CHANGELOG.txt to reflect the new 1.17 release.
drunken-monkey Mar 14, 2016
00795c1
Issue #2693425 by jojyja: Fixed a typo in search_api.info.
Mar 24, 2016
f362e5b
Added hook group info for three more hooks.
drunken-monkey Apr 4, 2016
91df710
Fixed reaction to updating of node access records.
drunken-monkey Apr 20, 2016
cd32d59
Fixed typo when checking for access to comments.
drunken-monkey Apr 20, 2016
560e623
Revised mechanism for passing (highlighted) field values to Views.
drunken-monkey Apr 20, 2016
33f2976
Adapted CHANGELOG.txt to release 1.18.
drunken-monkey Apr 21, 2016
0580e94
Issue #2419853 by drunken monkey: Fixed HTML filter leaves escaped en…
drunken-monkey Apr 21, 2016
2b33674
Issue #2703675 by drunken monkey, heykarthikwithu: Fixed accidental a…
drunken-monkey Apr 21, 2016
965bea1
Issue #2665970 by andrei.colesnic, drunken monkey: Added "Limit list …
Apr 21, 2016
02041b1
Issue #2700011 by drunken monkey: Fixed compatibility issues of facet…
drunken-monkey Apr 21, 2016
7a0b612
Issue #1889940 by cspurk, Yaron Tal: Fixed "HTML filter" processor to…
Apr 21, 2016
14b90c5
Issue #2700879 by drunken monkey: Fixed breadcrumbs on index tabs.
drunken-monkey Apr 22, 2016
cfc2bdf
Issue #2707039 by alan-ps: Fixed indexes of flag entities with "bundl…
Apr 22, 2016
196cd12
Issue #2710893 by alan-ps, drunken monkey: Fixed creation of comment …
May 5, 2016
d07089b
Issue #2720465 by drunken monkey: Fixed bundle filter's handling of e…
drunken-monkey Jun 7, 2016
99c23a3
Issue #2733447 by jsacksick: Fixed translatability of our Views taxon…
Jun 10, 2016
ff80f77
Issue #2742053 by tunic: Fixed change notification on node access rec…
Jun 11, 2016
d7360f7
Issue #2744995 by John Cook, drunken monkey: Fixed search views witho…
Jun 11, 2016
db8a350
Issue #2744189 by nikolabintev, drunken monkey: Fixed highlighting fo…
Jun 15, 2016
5890fa4
Issue #2724687 by StefanPr, drunken monkey: Fixed failed sanitization…
Jun 15, 2016
6db63ee
Adapted CHANGELOG.txt to 1.19 release.
drunken-monkey Jul 5, 2016
c3382e5
Back to dev version.
drunken-monkey Jul 5, 2016
9384359
Issue #2753441 by Johnny vd Laar: Fixed translated field names in lan…
Jul 6, 2016
893bd59
Issue #1818572 by morningtime, drunken monkey, lodey, guillaumev: Add…
Jul 19, 2016
20fc8aa
Issue #2731103 by drunken monkey: Fixed the default value for the tax…
drunken-monkey Jul 21, 2016
a093d89
Adapted CHANGELOG.txt to 1.20 release.
drunken-monkey Jul 21, 2016
0801154
Back to dev version.
drunken-monkey Jul 21, 2016
5cf51a0
Issue #2769877 by mfernea: Fixed database exception when filtering fo…
Jul 21, 2016
ef5fc55
Issue #2769021 by Plazik, drunken monkey: Added the generated Search …
Aug 1, 2016
4c53be2
Issue #2649412 by relaxnow, GoZ: Added support for minimum granularit…
Aug 22, 2016
3193709
Fixed literal use of 'item_id' in datasource instead of property.
drunken-monkey Aug 23, 2016
7b661ee
Issue #2779159 by mark_fullmer, drunken monkey: Added a Stemmer proce…
Oct 19, 2016
5447fe0
Issue #2358065 by Jelle_S, graper, drunken monkey: Added the option f…
Oct 19, 2016
88c0d9b
Issue #2778261 by drunken monkey, BAHbKA: Fixed "Index items immediat…
drunken-monkey Oct 26, 2016
8afc105
Issue #2822145 by drunken monkey: Fixed problem with phrase search in…
drunken-monkey Nov 12, 2016
c25e27b
Issue #2822836 by prince_zyxware: Fixed some Drupal coding standards …
Nov 12, 2016
ee67f12
Issue #2827717 by Fabien.Godineau, drunken monkey: Fixed disabling of…
Nov 26, 2016
e3c2e1f
Issue #2828380 by jansete: Fixed taxonomy term access tag in Views fi…
Nov 29, 2016
a49ceae
Issue #2632880 by drunken monkey, donquixote: Added possibility to ch…
drunken-monkey Dec 23, 2016
b33a62e
Issue #2836687 by sarthak drupal: Fixed one doc comment typo.
Jan 1, 2017
4ac425e
Issue #2838075 by dsnopek: Fixed possible race condition in hook_syst…
Jan 1, 2017
7b94a45
Issue #1670420 by kyletaylored, dorficus, drunken monkey: Fixed poten…
Jan 7, 2017
f03dc28
Issue #2840261 by alan-ps: Fixed usage of outdated hash functions.
Jan 22, 2017
2dea835
Issue #2833482 by drunken monkey: Fixed undefined constant when unins…
drunken-monkey Jan 25, 2017
7891a93
Issue #2837745 by drunken monkey, klausi: Fixed NULL tags on old seri…
drunken-monkey Jan 25, 2017
2c7aab9
Issue #2844990 by drunken monkey: Made the "Role filter" data alterat…
drunken-monkey Feb 6, 2017
fe202e7
Issue #2842856 by drunken monkey: Fixed language filters for "Multipl…
drunken-monkey Feb 6, 2017
ebe00d1
Issue #2765317 by JorgenSandstrom, NWOM, drunken monkey: Added a "Las…
Feb 16, 2017
c8a39a5
Issue #2780341 by Berdir: Fixed passing of custom ranges to date facets.
Feb 22, 2017
4489bb4
Issue #2574889 by drunken monkey, ChristianAdamski: Added Tour module…
drunken-monkey Feb 22, 2017
2b70f34
Revert "Issue #2574889 by drunken monkey, ChristianAdamski: Added Tou…
drunken-monkey Feb 22, 2017
bc9e81d
Adapted CHANGELOG.txt to 1.21 release.
drunken-monkey Feb 23, 2017
37e9a66
Back to dev version.
drunken-monkey Feb 23, 2017
4bd744f
Issue #2863445 by dbjpanda, drunken monkey: Fixed phrasing in README.…
Apr 20, 2017
7eef462
Issue #2855447 by mparker17, drunken monkey: Added "Separator" option…
Apr 25, 2017
c713dc7
Issue #2860624 by drunken monkey: Fixed problem with empty words in V…
drunken-monkey Apr 25, 2017
d41c745
Issue #2875793 by drunken monkey: Fixed buggy error handling in Views.
drunken-monkey May 12, 2017
a5ba346
Issue #2749963 by drunken monkey: Fixed "Index hierarchy" not having …
drunken-monkey May 25, 2017
df04cb1
Issue #2788593 by drunken monkey: Fixed error in Views query settings…
drunken-monkey May 26, 2017
41813a1
Issue #2879892 by blacklabel_tom, drunken monkey: Fixed link in descr…
Jun 18, 2017
a24146a
Issue #1710212 by drunken monkey: Added a data alteration for indexin…
drunken-monkey Jun 18, 2017
6bff579
Adapted CHANGELOG.txt to 1.22 release.
drunken-monkey Jul 18, 2017
8b46943
Back to dev version.
drunken-monkey Jul 18, 2017
b9b5fda
Issue #2904268 by pobster, drunken monkey: Added support for language…
pobtastic Sep 6, 2017
f838690
Issue #2905445 by ciss, drunken monkey: Fixed error handling in Views…
Sep 10, 2017
6b10116
Issue #2566529 by Dylan Donkersgoed, drunken monkey, joachim, swirt: …
Dec 4, 2017
40d143f
Issue #2928769 by jannis, drunken monkey: Fixed Views cache not being…
Dec 18, 2017
bd628b1
Issue #2927692 by drunken monkey, Kristi Wachter: Fixed exposed group…
drunken-monkey Dec 19, 2017
0d8fc5e
Issue #1393064 by xlyz, drunken monkey, jannis: Fixed handling of emp…
Jan 20, 2018
9fa4644
Issue #2889989 by kevineinarsson, drunken monkey, kristofferwiklund: …
Jan 21, 2018
eb15a80
Issue #1903004 by AndyF, joseph.olstad, drunken monkey: Fixed errors …
Feb 18, 2018
1054441
Issue #2949562 by DamienMcKenna, drunken monkey: Fixed stemming of mu…
Mar 4, 2018
eec1838
Adapted CHANGELOG.txt to 1.23 release.
drunken-monkey Mar 31, 2018
2744b4e
Back to dev version.
drunken-monkey Mar 31, 2018
66a8733
Issue #2958201 by jcnventura, drunken monkey: Reverted issue #2566529…
jcnventura Apr 5, 2018
2c89c8e
Adapted CHANGELOG.txt to 1.24 release.
drunken-monkey Apr 5, 2018
91747ce
Back to dev version.
drunken-monkey Apr 5, 2018
5f46652
Issue #2949899 by drunken monkey, DamienMcKenna: Added a warning agai…
drunken-monkey Apr 14, 2018
52d2245
Issue #2828883 by JorgenSandstrom, drunken monkey: Fixed property typ…
Apr 22, 2018
e899283
Issue Issue #2948820 by capysara, drunken monkey: Added a link to th…
Apr 22, 2018
8af7495
Issue #2408727 by drunken monkey, OliverColeman: Fixed out-of-memory …
drunken-monkey Jun 22, 2018
76ed48c
Issue #1783746 by das-peter, sammys, SpadXIII, drunken monkey, rulowe…
Jul 14, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 402 additions & 18 deletions CHANGELOG.txt

Large diffs are not rendered by default.

22 changes: 10 additions & 12 deletions README.txt
Original file line number Diff line number Diff line change
@@ -31,9 +31,9 @@ Terms as used in this module.
Sphinx or any other professional or simple indexing mechanism. Takes care of
the details of all operations, especially indexing or searching content.
- Server:
One specific place for indexing data, using a set service class. Can
e.g. be some tables in a database, a connection to a Solr server or other
external services, etc.
One specific place for indexing data, using a specific service class. For
example this could be some tables in a database, a connection to a Solr server
or other external services, etc.
- Index:
A configuration object for indexing data of a specific type. What and how data
is indexed is determined by its settings. Also keeps track of which items
@@ -90,7 +90,7 @@ IMPORTANT: Access checks
results are displayed – either by only indexing such items, or by filtering
appropriately at search time.
For search on general site content (item type "Node"), this is already
supported by the Search API. To enable this, go to the index's "Workflow" tab
supported by the Search API. To enable this, go to the index's "Filters" tab
and activate the "Node access" data alteration. This will add the necessary
field, "Node access information", to the index (which you have to leave as
"indexed"). If both this field and "Published" are set to be indexed, access
@@ -171,8 +171,8 @@ form at the bottom of the page. For instance, you might want to index the
author's username to the indexed data of a node, and you need to add the "Body"
entity to the node when you want to index the actual text it contains.

- Index workflow
(Configuration > Search API > [Index name] > Workflow)
- Indexing workflow
(Configuration > Search API > [Index name] > Filters)

This page lets you customize how the created index works, and what metadata will
be available, by selecting data alterations and processors (see the glossary for
@@ -210,12 +210,6 @@ search_api_index_worker_callback_runtime:
API will spend indexing (for all indexes combined) in each cron run. The
default is 15 seconds.

search_api_batch_per_cron:
By changing this variable, you can define how many batch items are created on
a single cron run. The value is per index, so on a site with 5 indexes with a
cron limit of 100 each, the default value of 10 will load and queue up to 5000
search items in up to 50 batch items.


Information for developers
--------------------------
@@ -391,6 +385,10 @@ Included components
Enables the admin to specify a stopwords file, the words contained in which
will be filtered out of the text data indexed. This can be used to exclude
too common words from indexing, for servers not supporting this natively.
* Stem words
Uses the PorterStemmer method to reduce words to stems. A search for
"garden" will return results for "gardening" and "garden," as will a search
for "gardening."

- Additional modules

90 changes: 78 additions & 12 deletions contrib/search_api_facetapi/plugins/facetapi/adapter.inc
Original file line number Diff line number Diff line change
@@ -61,6 +61,10 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
public function initActiveFilters($query) {
$search_id = $query->getOption('search id');
$index_id = $this->info['instance'];
// Only act on queries from the right index.
if ($index_id != $query->getIndex()->machine_name) {
return;
}
$facets = facetapi_get_enabled_facets($this->info['name']);
$this->fields = array();

@@ -78,21 +82,21 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// displayed.
$facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();

// Remember this search ID, if necessary.
$this->rememberSearchId($index_id, $search_id);

if (array_search($search_id, $facet_search_ids) === FALSE) {
$search_ids = variable_get('search_api_facets_search_ids', array());
if (empty($search_ids[$index_id][$search_id])) {
// Remember this search ID.
$search_ids[$index_id][$search_id] = $search_id;
variable_set('search_api_facets_search_ids', $search_ids);
}
if (!$default_true) {
continue; // We are only to show facets for explicitly named search ids.
// We are only to show facets for explicitly named search ids.
continue;
}
}
elseif ($default_true) {
continue; // The 'facet_search_ids' in the settings are to be excluded.
// The 'facet_search_ids' in the settings are to be excluded.
continue;
}
$active[$facet['name']] = $search_id;
$facet_key = $facet['name'] . '@' . $this->getSearcher();
$active[$facet_key] = $search_id;
$this->fields[$facet['name']] = array(
'field' => $facet['field'],
'limit' => $options['hard_limit'],
@@ -103,13 +107,35 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
}
}

/**
* Adds a search ID to the list of known searches for an index.
*
* @param string $index_id
* The machine name of the search index.
* @param string $search_id
* The identifier of the executed search.
*/
protected function rememberSearchId($index_id, $search_id) {
$search_ids = variable_get('search_api_facets_search_ids', array());
if (empty($search_ids[$index_id][$search_id])) {
$search_ids[$index_id][$search_id] = $search_id;
asort($search_ids[$index_id]);
variable_set('search_api_facets_search_ids', $search_ids);
}
}

/**
* Add the given facet to the query.
*/
public function addFacet(array $facet, SearchApiQueryInterface $query) {
if (isset($this->fields[$facet['name']])) {
$options = &$query->getOptions();
$options['search_api_facets'][$facet['name']] = $this->fields[$facet['name']];
$facet_info = $this->fields[$facet['name']];
if (!empty($facet['query_options'])) {
// Let facet-specific query options override the set options.
$facet_info = $facet['query_options'] + $facet_info;
}
$options['search_api_facets'][$facet['name']] = $facet_info;
}
}

@@ -139,7 +165,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// I suspect that http://drupal.org/node/593658 would help.
// For now, just taking the first current search for this index. :-/
foreach (search_api_current_search() as $search) {
list($query, $results) = $search;
list($query) = $search;
if ($query->getIndex()->machine_name == $index_id) {
$this->current_search = $search;
}
@@ -166,6 +192,12 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
*/
public function getSearchKeys() {
$search = $this->getCurrentSearch();

// If the search is empty then there's no reason to continue.
if (!$search) {
return NULL;
}

$keys = $search[0]->getOriginalKeys();
if (is_array($keys)) {
// This will happen nearly never when displaying the search keys to the
@@ -196,7 +228,6 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
*/
public function settingsForm(&$form, &$form_state) {
$facet = $form['#facetapi']['facet'];
$realm = $form['#facetapi']['realm'];
$facet_settings = $this->getFacet($facet)->getSettings();
$options = $facet_settings->settings;
$search_ids = variable_get('search_api_facets_search_ids', array());
@@ -205,6 +236,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
$form['global']['default_true'] = array(
'#type' => 'select',
'#title' => t('Display for searches'),
'#prefix' => '<div class="facetapi-global-setting">',
'#options' => array(
TRUE => t('For all except the selected'),
FALSE => t('Only for the selected'),
@@ -214,6 +246,7 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
$form['global']['facet_search_ids'] = array(
'#type' => 'select',
'#title' => t('Search IDs'),
'#suffix' => '</div>',
'#options' => $search_ids,
'#size' => min(4, count($search_ids)),
'#multiple' => TRUE,
@@ -246,9 +279,42 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
'#type' => 'select',
'#title' => t('Granularity'),
'#description' => t('Determine the maximum drill-down level'),
'#prefix' => '<div class="facetapi-global-setting">',
'#suffix' => '</div>',
'#options' => $granularity_options,
'#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE,
);

// Date facets don't support the "OR" operator (for now).
$form['global']['operator']['#access'] = FALSE;

$default_value = FACETAPI_DATE_YEAR;
if (isset($options['date_granularity_min'])) {
$default_value = $options['date_granularity_min'];
}
$form['global']['date_granularity_min'] = array(
'#type' => 'select',
'#title' => t('Minimum granularity'),
'#description' => t('Determine the minimum drill-down level to start at'),
'#prefix' => '<div class="facetapi-global-setting">',
'#suffix' => '</div>',
'#options' => $granularity_options,
'#default_value' => $default_value,
);
}

// Add an "Exclude" option for terms.
if (!empty($facet['query types']) && in_array('term', $facet['query types'])) {
$form['global']['operator']['#weight'] = -2;
unset($form['global']['operator']['#suffix']);
$form['global']['exclude'] = array(
'#type' => 'checkbox',
'#title' => t('Exclude'),
'#description' => t('Make the search exclude selected facets, instead of restricting it to them.'),
'#suffix' => '</div>',
'#weight' => -1,
'#default_value' => !empty($options['exclude']),
);
}
}
}
247 changes: 192 additions & 55 deletions contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
Original file line number Diff line number Diff line change
@@ -37,6 +37,17 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
public function execute($query) {
// Return terms for this facet.
$this->adapter->addFacet($this->facet, $query);

$settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;

// First check if the facet is enabled for this search.
$default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
$facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
// Facet is not enabled for this search ID.
return;
}

// Change limit to "unlimited" (-1).
$options = &$query->getOptions();
if (!empty($options['search_api_facets'][$this->facet['name']])) {
@@ -46,14 +57,145 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
if ($active = $this->adapter->getActiveItems($this->facet)) {
$item = end($active);
$field = $this->facet['field'];
$regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE);
$filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']);
$this->addFacetFilter($query, $field, $filter);
$filter = $this->createRangeFilter($item['value']);
if ($filter) {
$this->addFacetFilter($query, $field, $filter);
}
}
}

/**
* Rewrites the handler-specific date range syntax to the normal facet syntax.
*
* @param string $value
* The user-facing facet value.
*
* @return string|null
* A facet to add as a filter, in the format used internally in this module.
* Or NULL if the raw facet in $value is not valid.
*/
protected function createRangeFilter($value) {
// Ignore any filters passed directly from the server (range or missing).
if (!$value || $value == '!' || (!ctype_digit($value[0]) && preg_match('/^[\[(][^ ]+ TO [^ ]+[\])]$/', $value))) {
return $value ? $value : NULL;
}

// Parse into date parts.
$parts = $this->parseRangeFilter($value);

// Return NULL if the date parts are invalid or none were found.
if (empty($parts)) {
return NULL;
}

$date = new DateTime();
switch (count($parts)) {
case 1:
$date->setDate($parts[0], 1, 1);
$date->setTime(0, 0, 0);
$lower = $date->format('U');
$date->setDate($parts[0] + 1, 1, 1);
$date->setTime(0, 0, -1);
$upper = $date->format('U');
break;

case 2:
// Luckily, $month = 13 is treated as January of next year. (The same
// goes for all other parameters.) We use the inverse trick for the
// seconds of the upper bound, since that's inclusive and we want to
// stop at a second before the next segment starts.
$date->setDate($parts[0], $parts[1], 1);
$date->setTime(0, 0, 0);
$lower = $date->format('U');
$date->setDate($parts[0], $parts[1] + 1, 1);
$date->setTime(0, 0, -1);
$upper = $date->format('U');
break;

case 3:
$date->setDate($parts[0], $parts[1], $parts[2]);
$date->setTime(0, 0, 0);
$lower = $date->format('U');
$date->setDate($parts[0], $parts[1], $parts[2] + 1);
$date->setTime(0, 0, -1);
$upper = $date->format('U');
break;

case 4:
$date->setDate($parts[0], $parts[1], $parts[2]);
$date->setTime($parts[3], 0, 0);
$lower = $date->format('U');
$date->setTime($parts[3] + 1, 0, -1);
$upper = $date->format('U');
break;

case 5:
$date->setDate($parts[0], $parts[1], $parts[2]);
$date->setTime($parts[3], $parts[4], 0);
$lower = $date->format('U');
$date->setTime($parts[3], $parts[4] + 1, -1);
$upper = $date->format('U');
break;

case 6:
$date->setDate($parts[0], $parts[1], $parts[2]);
$date->setTime($parts[3], $parts[4], $parts[5]);
return $date->format('U');

default:
return $value;
}

return "[$lower TO $upper]";
}

/**
* Parses the date range filter value into parts.
*
* @param string $value
* The user-facing facet value.
*
* @return int[]|null
* An array of date parts, or NULL if an invalid value was provided.
*/
protected static function parseRangeFilter($value) {
$parts = explode('-', $value);

foreach ($parts as $i => $part) {
// Invalidate if part is not an integer.
if ($part === '' || !is_numeric($part) || intval($part) != $part) {
return NULL;
}
$parts[$i] = (int) $part;
// Depending on the position, negative numbers or 0 are invalid.
switch ($i) {
case 0:
// Years can contain anything – negative values are unlikely, but
// technically possible.
break;
case 1:
case 2:
// Days and months have to be positive.
if ($part <= 0) {
return NULL;
}
break;
default:
// All others can be 0, but not negative.
if ($part < 0) {
return NULL;
}
}
}

return $parts;
}

/**
* Replacement callback for replacing ISO dates with timestamps.
*
* Not used anymore, but kept for backwards compatibility with potential
* subclasses.
*/
public function replaceDateString($matches) {
return strtotime($matches[0]);
@@ -68,22 +210,17 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
public function build() {
$facet = $this->adapter->getFacet($this->facet);
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
$facet_key = $facet['name'] . '@' . $this->adapter->getSearcher();
if (empty($search_ids[$facet_key]) || !search_api_current_search($search_ids[$facet_key])) {
return array();
}
$search_id = $search_ids[$facet['name']];
$search_id = $search_ids[$facet_key];
$build = array();
$search = search_api_current_search($search_id);
$results = $search[1];
if (!$results['result count']) {
return array();
}
// Gets total number of documents matched in search.
$total = $results['result count'];

// Most of the code below is copied from search_facetapi's implementation of
// this method.

// Executes query, iterates over results.
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
$values = $results['search_api_facets'][$this->facet['name']];
@@ -102,37 +239,46 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
}
}
else {
$filter = substr($value['filter'], 1, -1);
$pos = strpos($filter, ' ');
if ($pos !== FALSE) {
$lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY);
$upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY);
$filter = '[' . $lower . ' TO ' . $upper . ']';
}
$build[$filter]['#count'] = $value['count'];
}
}
}
}

// Get the finest level of detail we're allowed to drill down to.
$settings = $facet->getSettings()->settings;
$granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;

// Get the finest level of detail we're allowed to drill down to.
$max_granularity = FACETAPI_DATE_MINUTE;
if (isset($settings['date_granularity'])) {
$max_granularity = $settings['date_granularity'];
}

// Get the coarsest level of detail we're allowed to start at.
$min_granularity = FACETAPI_DATE_YEAR;
if (isset($settings['date_granularity_min'])) {
$min_granularity = $settings['date_granularity_min'];
}

// Gets active facets, starts building hierarchy.
$parent = $gap = NULL;
foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) {
$parent = $granularity = NULL;
$active_items = $this->adapter->getActiveItems($this->facet);
foreach ($active_items as $value => $item) {
// If the item is active, the count is the result set count.
$build[$value] = array('#count' => $total);

// Gets next "gap" increment.
if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
// Gets next "gap" increment. Ignore any filters passed directly from the
// server (range or missing). We always create filters starting with a
// year.
$value = "$value";
if (!$value || !ctype_digit($value[0])) {
continue;
}

$granularity = search_api_facetapi_date_get_granularity($value);
if (!$granularity) {
continue;
}
$start = substr($value, 1, $pos);
$end = substr($value, $pos + 4, -1);
$date_gap = facetapi_get_date_gap($start, $end);
$gap = facetapi_get_next_date_gap($date_gap, $granularity);
$granularity = facetapi_get_next_date_gap($granularity, $max_granularity);

// If there is a previous item, there is a parent, uses a reference so the
// arrays are populated when they are updated.
@@ -144,6 +290,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
// Stores the last value iterated over.
$parent = $value;
}

if (empty($raw_values)) {
return $build;
}
@@ -153,7 +300,7 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
$timestamps = array_keys($raw_values);
if (NULL === $parent) {
if (count($raw_values) > 1) {
$gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
$granularity = facetapi_get_timestamp_gap(min($timestamps), max($timestamps), $max_granularity);
// Array of numbers used to determine whether the next gap is smaller than
// the minimum gap allowed in the drilldown.
$gap_numbers = array(
@@ -164,42 +311,31 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue
FACETAPI_DATE_MINUTE => 2,
FACETAPI_DATE_SECOND => 1,
);
// Gets gap numbers for both the gap and minimum gap, checks if the gap
// is within the limit set by the $granularity parameter.
if ($gap_numbers[$gap] < $gap_numbers[$granularity]) {
$gap = $granularity;
// Gets gap numbers for both the gap, minimum and maximum gap, checks if
// the gap is within the limit set by the $granularity parameters.
if ($gap_numbers[$granularity] < $gap_numbers[$max_granularity]) {
$granularity = $max_granularity;
}
if ($gap_numbers[$granularity] > $gap_numbers[$min_granularity]) {
$granularity = $min_granularity;
}
}
else {
$gap = $granularity;
$granularity = $max_granularity;
}
}

// Converts all timestamps to dates in ISO 8601 format.
$dates = array();
foreach ($timestamps as $timestamp) {
$dates[$timestamp] = facetapi_isodate($timestamp, $gap);
}

// Treat each date as the range start and next date as the range end.
$range_end = array();
$previous = NULL;
foreach (array_unique($dates) as $date) {
if (NULL !== $previous) {
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
}
$previous = $date;
}
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);

// Groups dates by the range they belong to, builds the $build array
// with the facet counts and formatted range values.
// Groups dates by the range they belong to, builds the $build array with
// the facet counts and formatted range values.
$format = search_api_facetapi_date_get_granularity_format($granularity);
foreach ($raw_values as $value => $count) {
$new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
$new_value = date($format, $value);
if (!isset($build[$new_value])) {
$build[$new_value] = array('#count' => $count);
}
else {
// Active items already have their value set because it's the current
// result count.
elseif (!isset($active_items[$new_value])) {
$build[$new_value]['#count'] += $count;
}

@@ -212,4 +348,5 @@ class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQue

return $build;
}

}
111 changes: 88 additions & 23 deletions contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
Original file line number Diff line number Diff line change
@@ -30,53 +30,98 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
// Return terms for this facet.
$this->adapter->addFacet($this->facet, $query);

$settings = $this->adapter->getFacet($this->facet)->getSettings();
// Adds the operator parameter.
$operator = $settings->settings['operator'];
$settings = $this->getSettings()->settings;

// First check if the facet is enabled for this search.
$default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
$facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
// Facet is not enabled for this search ID.
return;
}

// Add active facet filters.
// Retrieve the active facet filters.
$active = $this->adapter->getActiveItems($this->facet);
if (empty($active)) {
return;
}

if (FACETAPI_OPERATOR_OR == $operator) {
// If we're dealing with an OR facet, we need to use a nested filter.
$facet_filter = $query->createFilter('OR');
// Create the facet filter, and add a tag to it so that it can be easily
// identified down the line by services when they need to exclude facets.
$operator = $settings['operator'];
if ($operator == FACETAPI_OPERATOR_AND) {
$conjunction = 'AND';
}
elseif ($operator == FACETAPI_OPERATOR_OR) {
$conjunction = 'OR';
// When the operator is OR, remove parent terms from the active ones if
// children are active. If we don't do this, sending a term and its
// parent will produce the same results as just sending the parent.
if (is_callable($this->facet['hierarchy callback']) && !$settings['flatten']) {
// Check the filters in reverse order, to avoid checking parents that
// will afterwards be removed anyways.
$values = array_keys($active);
$parents = call_user_func($this->facet['hierarchy callback'], $values);
foreach (array_reverse($values) as $filter) {
// Skip this filter if it was already removed, or if it is the
// "missing value" filter ("!").
if (!isset($active[$filter]) || !is_numeric($filter)) {
continue;
}
// Go through the entire hierarchy of the value and remove all its
// ancestors.
while (!empty($parents[$filter])) {
$ancestor = array_shift($parents[$filter]);
if (isset($active[$ancestor])) {
unset($active[$ancestor]);
if (!empty($parents[$ancestor])) {
$parents[$filter] = array_merge($parents[$filter], $parents[$ancestor]);
}
}
}
}
}
}
else {
// Otherwise we set the conditions directly on the query.
$facet_filter = $query;
$vars = array(
'%operator' => $operator,
'%facet' => !empty($this->facet['label']) ? $this->facet['label'] : $this->facet['name'],
);
watchdog('search_api_facetapi', 'Unknown facet operator %operator used for facet %facet.', $vars, WATCHDOG_WARNING);
return;
}
$tags = array('facet:' . $this->facet['field']);
$facet_filter = $query->createFilter($conjunction, $tags);

foreach ($active as $filter => $filter_array) {
$field = $this->facet['field'];
$this->addFacetFilter($facet_filter, $field, $filter);
}

// For OR facets, we now have to add the filter to the query.
if (FACETAPI_OPERATOR_OR == $operator) {
$query->filter($facet_filter);
}
// Now add the filter to the query.
$query->filter($facet_filter);
}

/**
* Helper method for setting a facet filter on a query or query filter object.
*/
protected function addFacetFilter($query_filter, $field, $filter) {
// Integer (or other nun-string) filters might mess up some of the following
// Test if this filter should be negated.
$settings = $this->adapter->getFacet($this->facet)->getSettings();
$exclude = !empty($settings->settings['exclude']);
// Integer (or other non-string) filters might mess up some of the following
// comparison expressions.
$filter = (string) $filter;
if ($filter == '!') {
$query_filter->condition($field, NULL);
$query_filter->condition($field, NULL, $exclude ? '<>' : '=');
}
elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
elseif ($filter && $filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
$lower = trim(substr($filter, 1, $pos));
$upper = trim(substr($filter, $pos + 4, -1));
if ($lower == '*' && $upper == '*') {
$query_filter->condition($field, NULL, '<>');
$query_filter->condition($field, NULL, $exclude ? '=' : '<>');
}
else {
elseif (!$exclude) {
if ($lower != '*') {
// Iff we have a range with two finite boundaries, we set two
// conditions (larger than the lower bound and less than the upper
@@ -92,9 +137,22 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
$query_filter->condition($field, $upper, '<=');
}
}
else {
// Same as above, but with inverted logic.
if ($lower != '*') {
if ($upper != '*' && ($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
$original_query_filter = $query_filter;
$query_filter = new SearchApiQueryFilter('OR');
}
$query_filter->condition($field, $lower, '<');
}
if ($upper != '*') {
$query_filter->condition($field, $upper, '>');
}
}
}
else {
$query_filter->condition($field, $filter);
$query_filter->condition($field, $filter, $exclude ? '<>' : '=');
}
if (isset($original_query_filter)) {
$original_query_filter->filter($query_filter);
@@ -113,13 +171,20 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
// initActiveFilters) so that we can retrieve it here and get the correct
// current search for this facet.
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
$facet_key = $facet['name'] . '@' . $this->adapter->getSearcher();
if (empty($search_ids[$facet_key]) || !search_api_current_search($search_ids[$facet_key])) {
return array();
}
$search_id = $search_ids[$facet['name']];
$search = search_api_current_search($search_id);
$search_id = $search_ids[$facet_key];
list(, $results) = search_api_current_search($search_id);
$build = array();
$results = $search[1];

// Always include the active facet items.
foreach ($this->adapter->getActiveItems($this->facet) as $filter) {
$build[$filter['value']]['#count'] = 0;
}

// Then, add the facets returned by the server.
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
$values = $results['search_api_facets'][$this->facet['name']];
foreach ($values as $value) {
34 changes: 33 additions & 1 deletion contrib/search_api_facetapi/search_api_facetapi.install
Original file line number Diff line number Diff line change
@@ -5,9 +5,41 @@
* Install, update and uninstall functions for the Search facets module.
*/

/**
* Implements hook_install().
*/
function search_api_facetapi_install() {
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR, 'Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH, 'F Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY, 'F j, Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR, 'H:__');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE, 'H:i');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND, 'H:i:S');
}

/**
* Implements hook_uninstall().
*/
function search_api_facetapi_uninstall() {
variable_del('search_api_facets_search_ids');
}
// We have to use the literal values here, as the Facet API module could have
// already been disabled at this point.
variable_del('date_format_search_api_facetapi_YEAR');
variable_del('date_format_search_api_facetapi_MONTH');
variable_del('date_format_search_api_facetapi_DAY');
variable_del('date_format_search_api_facetapi_HOUR');
variable_del('date_format_search_api_facetapi_MINUTE');
variable_del('date_format_search_api_facetapi_SECOND');
}

/**
* Set up date formats.
*/
function search_api_facetapi_update_7101() {
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_YEAR, 'Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MONTH, 'F Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_DAY, 'F j, Y');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_HOUR, 'H:__');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_MINUTE, 'H:i');
variable_set('date_format_search_api_facetapi_' . FACETAPI_DATE_SECOND, 'H:i:S');
}
279 changes: 263 additions & 16 deletions contrib/search_api_facetapi/search_api_facetapi.module
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ function search_api_facetapi_facetapi_searcher_info() {
$info = array();
$indexes = search_api_index_load_multiple(FALSE);
foreach ($indexes as $index) {
if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
if (_search_api_facetapi_index_support_feature($index)) {
$searcher_name = 'search_api@' . $index->machine_name;
$info[$searcher_name] = array(
'label' => t('Search service: @name', array('@name' => $index->name)),
@@ -92,12 +92,12 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
// other modules.
$type_settings = array(
'taxonomy_term' => array(
'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy',
'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
),
'date' => array(
'query type' => 'date',
'map options' => array(
'map callback' => 'facetapi_map_date',
'map callback' => 'search_api_facetapi_map_date',
),
),
);
@@ -116,7 +116,7 @@ function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
'description' => t('Filter by @type.', array('@type' => $field['name'])),
'allowed operators' => array(
FACETAPI_OPERATOR_AND => TRUE,
FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
FACETAPI_OPERATOR_OR => _search_api_facetapi_index_support_feature($index, 'search_api_facets_operator_or'),
),
'dependency plugins' => array('role'),
'facet missing allowed' => TRUE,
@@ -211,21 +211,115 @@ function search_api_facetapi_search_api_query_alter($query) {
}
}

/**
* Implements hook_date_formats().
*/
function search_api_facetapi_date_formats() {
return array(
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_YEAR,
'format' => 'Y',
'locales' => array(),
),
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_MONTH,
'format' => 'F Y',
'locales' => array(),
),
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_DAY,
'format' => 'F j, Y',
'locales' => array(),
),
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_HOUR,
'format' => 'H:__',
'locales' => array(),
),
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_MINUTE,
'format' => 'H:i',
'locales' => array(),
),
array(
'type' => 'search_api_facetapi_' . FACETAPI_DATE_SECOND,
'format' => 'H:i:s',
'locales' => array(),
),
);
}

/**
* Implements hook_date_format_types().
*/
function search_api_facetapi_date_format_types() {
return array(
'search_api_facetapi_' . FACETAPI_DATE_YEAR => t('Search facets - Years'),
'search_api_facetapi_' . FACETAPI_DATE_MONTH => t('Search facets - Months'),
'search_api_facetapi_' . FACETAPI_DATE_DAY => t('Search facets - Days'),
'search_api_facetapi_' . FACETAPI_DATE_HOUR => t('Search facets - Hours'),
'search_api_facetapi_' . FACETAPI_DATE_MINUTE => t('Search facets - Minutes'),
'search_api_facetapi_' . FACETAPI_DATE_SECOND => t('Search facets - Seconds'),
);
}

/**
* Menu callback for the facet settings page.
*/
function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
if (!$index->enabled) {
return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
}
if (!$index->server()->supportsFeature('search_api_facets')) {
if (!_search_api_facetapi_index_support_feature($index)) {
return array('#markup' => t('This index uses a server that does not support facet functionality.'));
}
$searcher_name = 'search_api@' . $index->machine_name;
module_load_include('inc', 'facetapi', 'facetapi.admin');
return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
}

/**
* Checks whether a certain feature is supported for an index.
*
* @param SearchApiIndex $index
* The search index which should be checked.
* @param string $feature
* (optional) The feature to check for. Defaults to "search_api_facets".
*
* @return bool
* TRUE if the feature is supported by the index's server (and the index is
* currently enabled), FALSE otherwise.
*/
function _search_api_facetapi_index_support_feature(SearchApiIndex $index, $feature = 'search_api_facets') {
try {
$server = $index->server();
return $server && $server->supportsFeature($feature);
}
catch (SearchApiException $e) {
return FALSE;
}
}

/**
* Gets hierarchy information for taxonomy terms.
*
* Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
*
* Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
* our special "!" value is not passed.
*
* @param array $values
* An array containing the term IDs.
*
* @return array
* An associative array mapping term IDs to parent IDs (where parents could be
* found).
*/
function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
$values = array_filter($values, 'is_numeric');
return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
}

/**
* Map callback for all search_api facet fields.
*
@@ -298,13 +392,13 @@ function search_api_facetapi_facet_map_callback(array $values, array $options =
$lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
$upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
if ($lower == '*' && $upper == '*') {
$map[$value] = t('any');
$map[$value] = t('any');
}
elseif ($lower == '*') {
$map[$value] = "< $upper";
$map[$value] = " $upper";
}
elseif ($upper == '*') {
$map[$value] = "> $lower";
$map[$value] = " $lower";
}
else {
$map[$value] = "$lower – $upper";
@@ -316,25 +410,49 @@ function search_api_facetapi_facet_map_callback(array $values, array $options =

/**
* Creates a human-readable label for single facet filter values.
*
* @param array $values
* The values for which labels should be returned.
* @param array $options
* An associative array containing the following information about the facet:
* - field: Field information, as stored in the index, but with an additional
* "key" property set to the field's internal name.
* - index id: The machine name of the index for this facet.
* - map callback: (optional) A callback that will be called at the beginning,
* which allows initial mapping of filters. Only values not mapped by that
* callback will be processed by this method.
* - value callback: A callback used to map single values and the limits of
* ranges. The signature is the same as for this function, but all values
* will be single values.
* - missing label: (optional) The label used for the "missing" facet.
*
* @return array
* An array mapping raw facet values to their labels.
*/
function _search_api_facetapi_facet_create_label(array $values, array $options) {
$field = $options['field'];
$map = array();
$n = count($values);

// For entities, we can simply use the entity labels.
if (isset($field['entity_type'])) {
$type = $field['entity_type'];
$entities = entity_load($type, $values);
$map = array();
foreach ($entities as $id => $entity) {
$label = entity_label($type, $entity);
if ($label) {
if ($label !== FALSE) {
$map[$id] = $label;
}
}
return $map;
if (count($map) == $n) {
return $map;
}
}

// Then, we check whether there is an options list for the field.
$index = search_api_index_load($options['index id']);
$wrapper = $index->entityWrapper();
$values = drupal_map_assoc($values);
foreach (explode(':', $field['key']) as $part) {
if (!isset($wrapper->$part)) {
$wrapper = NULL;
@@ -345,12 +463,18 @@ function _search_api_facetapi_facet_create_label(array $values, array $options)
$wrapper = $wrapper[0];
}
}
if ($wrapper && ($options = $wrapper->optionsList('view'))) {
return $options;
if ($wrapper && ($options_list = $wrapper->optionsList('view'))) {
// We have no use for empty strings, as then the facet links would be
// invisible.
$map += array_intersect_key(array_filter($options_list, 'strlen'), $values);
if (count($map) == $n) {
return $map;
}
}
// As a "last resort" we try to create a label based on the field type.
$map = array();
foreach ($values as $value) {

// As a "last resort" we try to create a label based on the field type, for
// all values that haven't got a mapping yet.
foreach (array_diff_key($values, $map) as $value) {
switch ($field['type']) {
case 'boolean':
$map[$value] = $value ? t('true') : t('false');
@@ -382,3 +506,126 @@ function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_
$cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
cache_clear_all($cid, 'cache', TRUE);
}

/**
* Computes the granularity of a date facet filter.
*
* @param $filter
* The filter value to examine.
*
* @return string|null
* Either one of the FACETAPI_DATE_* constants corresponding to the
* granularity of the filter, or NULL if it couldn't be computed.
*/
function search_api_facetapi_date_get_granularity($filter) {
// Granularity corresponds to number of dashes in filter value.
$units = array(
FACETAPI_DATE_YEAR,
FACETAPI_DATE_MONTH,
FACETAPI_DATE_DAY,
FACETAPI_DATE_HOUR,
FACETAPI_DATE_MINUTE,
FACETAPI_DATE_SECOND,
);
$count = substr_count($filter, '-');
return isset($units[$count]) ? $units[$count] : NULL;
}

/**
* Returns the date format used for a given granularity.
*
* @param $granularity
* One of the FACETAPI_DATE_* constants.
*
* @return string
* The date format used for the given granularity.
*/
function search_api_facetapi_date_get_granularity_format($granularity) {
$formats = array(
FACETAPI_DATE_YEAR => 'Y',
FACETAPI_DATE_MONTH => 'Y-m',
FACETAPI_DATE_DAY => 'Y-m-d',
FACETAPI_DATE_HOUR => 'Y-m-d-H',
FACETAPI_DATE_MINUTE => 'Y-m-d-H-i',
FACETAPI_DATE_SECOND => 'Y-m-d-H-i-s',
);
return $formats[$granularity];
}

/**
* Constructs labels for date facet filter values.
*
* @param array $values
* The date facet filter values, as used in URL parameters.
* @param array $options
* (optional) Options for creating the mapping. The following options are
* recognized:
* - format callback: A callback for creating a label for a timestamp. The
* function signature is like search_api_facetapi_format_timestamp(),
* receiving a timestamp and one of the FACETAPI_DATE_* constants as the
* parameters and returning a human-readable label.
*
* @return array
* An array of labels for the given facet filters.
*/
function search_api_facetapi_map_date(array $values, array $options = array()) {
$map = array();
foreach ($values as $value) {
// Ignore any filters passed directly from the server (range or missing). We
// always create filters starting with a year.
$value = "$value";
if (!$value || !ctype_digit($value[0])) {
continue;
}

// Get the granularity of the filter.
$granularity = search_api_facetapi_date_get_granularity($value);
if (!$granularity) {
continue;
}

// Otherwise, parse the timestamp from the known format and format it as a
// label.
$format = search_api_facetapi_date_get_granularity_format($granularity);
// Use the "!" modifier to make the date parsing independent of the current
// date/time. (See #2678856.)
$date = DateTime::createFromFormat('!' . $format, $value);
if (!$date) {
continue;
}
$format_callback = 'search_api_facetapi_format_timestamp';
if (!empty($options['format callback']) && is_callable($options['format callback'])) {
$format_callback = $options['format callback'];
}
$map[$value] = call_user_func($format_callback, $date->format('U'), $granularity);
}
return $map;
}

/**
* Format a date according to the default timezone and the given precision.
*
* @param int $timestamp
* An integer containing the Unix timestamp being formated.
* @param string $precision
* A string containing the formatting precision. See the FACETAPI_DATE_*
* constants for valid values.
*
* @return string
* A human-readable representation of the timestamp.
*/
function search_api_facetapi_format_timestamp($timestamp, $precision = FACETAPI_DATE_YEAR) {
$formats = array(
FACETAPI_DATE_YEAR,
FACETAPI_DATE_MONTH,
FACETAPI_DATE_DAY,
FACETAPI_DATE_HOUR,
FACETAPI_DATE_MINUTE,
FACETAPI_DATE_SECOND,
);

if (!in_array($precision, $formats)) {
$precision = FACETAPI_DATE_YEAR;
}
return format_date($timestamp, 'search_api_facetapi_' . $precision);
}
31 changes: 31 additions & 0 deletions contrib/search_api_views/README.txt
Original file line number Diff line number Diff line change
@@ -24,6 +24,37 @@ When these are present, the normal keywords should be ignored and the related
items be returned as results instead. Sorting, filtering and range restriction
should all work normally.

"Random sort" feature
---------------------
This module defines the "Random sort" feature (feature key:
"search_api_random_sort") that allows to randomly sort the results returned by a
search. With a server supporting this, you can use the "Global: Random" sort to
sort the view's results randomly. Every time the query is run a different
sorting will be provided.

For developers:
A service class that wants to support this feature has to check for a
"search_api_random" field in the search query's sorts and insert a random sort
in that position. If the query is sorted in this way, then the
"search_api_random_sort" query option can contain additional options for the
random sort, as an associative array with any of the following keys:
- seed: A numeric seed value to use for the random sort.

"BETWEEN operator" feature
--------------------------
This module defines the "BETWEEN operator" feature (feature key:
"search_api_between") that adds the "BETWEEN" and "NOT BETWEEN" filter
operators to search queries. If your search server supports this feature, you
can use the "Is between" and "Is not between" operators when adding Views
filters for numeric, string or date types.

For developers:
A service class that wants to support this feature has to accept "BETWEEN" and
"NOT BETWEEN" as additional $operator values in query conditions. The value in
both cases is an array with the keys 0 and 1, with the value under key 0 being
the lower and the value under key 1 being the upper bound for the range in which
the field's value should ("BETWEEN") or should not ("NOT BETWEEN") be.

"Facets block" display
----------------------
Most features should be clear to users of Views. However, the module also
56 changes: 48 additions & 8 deletions contrib/search_api_views/includes/display_facet_block.inc
Original file line number Diff line number Diff line change
@@ -151,11 +151,9 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
}

public function execute() {
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
return NULL;
}
public function query() {
parent::query();

$facet_field = $this->get_option('facet_field');
if (!$facet_field) {
return NULL;
@@ -165,7 +163,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
if (!$base_path) {
$base_path = $_GET['q'];
}
$this->view->build();

$limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page'];
$query_options = &$this->view->query->getOptions();
if (!$this->get_option('hide_block')) {
@@ -179,6 +177,17 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
}
$query_options['search_api_base_path'] = $base_path;
$this->view->query->range(0, 0);
}

public function render() {
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
return NULL;
}
$facet_field = $this->get_option('facet_field');
if (!$facet_field) {
return NULL;
}

$this->view->execute();

@@ -229,7 +238,7 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
// Initializes variables passed to theme hook.
$variables = array(
'text' => $name,
'path' => $base_path,
'path' => $this->view->query->getOption('search_api_base_path'),
'count' => $term['count'],
'options' => array(
'attributes' => array('class' => 'facetapi-inactive'),
@@ -238,6 +247,31 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
),
);

// Override the $variables['#path'] if facetapi_pretty_paths is enabled.
if (module_exists('facetapi_pretty_paths')) {
// Get the appropriate facet adapter.
$adapter = facetapi_adapter_load('search_api@' . $index->machine_name);

// Get the URL processor and check if it uses pretty paths.
$urlProcessor = $adapter->getUrlProcessor();
if ($urlProcessor instanceof FacetapiUrlProcessorPrettyPaths) {
// Retrieve the pretty path alias from the URL processor.
$facet = facetapi_facet_load($facet_field, 'search_api@' . $index->machine_name);
$values = array(trim($term['filter'], '"'));

// Get the pretty path for the facet and remove the current search's
// base path from it.
$base_path_current = $urlProcessor->getBasePath();
$pretty_path = $urlProcessor->getFacetPath($facet, $values, FALSE);
$pretty_path = str_replace($base_path_current, '', $pretty_path);

// Set the new, pretty path for the facet and remove the "f" query
// parameter.
$variables['path'] = $variables['path'] . $pretty_path;
unset($variables['options']['query']['f']);
}
}

// Themes the link, adds row to facets.
$facets[] = array(
'class' => array('leaf'),
@@ -249,10 +283,16 @@ class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
return NULL;
}

$info['content']['facets'] = array(
return array(
'facets' => array(
'#theme' => 'item_list',
'#items' => $facets,
)
);
}

public function execute() {
$info['content'] = $this->render();
$info['content']['more'] = $this->render_more_link();
$info['subject'] = filter_xss_admin($this->view->get_title());
return $info;
16 changes: 16 additions & 0 deletions contrib/search_api_views/includes/handler_argument.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerArgument.
*/

/**
* Views argument handler class for handling all non-fulltext types.
*/
@@ -12,6 +17,17 @@ class SearchApiViewsHandlerArgument extends views_handler_argument {
*/
public $query;

/**
* The operator to use for multiple arguments.
*
* Either "and" or "or".
*
* @var string
*
* @see views_break_phrase
*/
public $operator;

/**
* Determine if the argument can generate a breadcrumb
*
161 changes: 161 additions & 0 deletions contrib/search_api_views/includes/handler_argument_date.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

/**
* @file
* Contains the SearchApiViewsHandlerArgumentDate class.
*/

/**
* Defines a contextual filter searching for a date or date range.
*/
class SearchApiViewsHandlerArgumentDate extends SearchApiViewsHandlerArgument {

/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
if (empty($this->value)) {
$this->fillValue();
if ($this->value === FALSE) {
$this->abort();
return;
}
}

$outer_conjunction = strtoupper($this->operator);

if (empty($this->options['not'])) {
$operator = '=';
$inner_conjunction = 'OR';
}
else {
$operator = '<>';
$inner_conjunction = 'AND';
}

if (!empty($this->value)) {
if (!empty($this->value)) {
$outer_filter = $this->query->createFilter($outer_conjunction);
foreach ($this->value as $value) {
$value_filter = $this->query->createFilter($inner_conjunction);
$values = explode(';', $value);
$values = array_map(array($this, 'getTimestamp'), $values);
if (in_array(FALSE, $values, TRUE)) {
$this->abort();
return;
}
$is_range = (count($values) > 1);

$inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter);
$range_op = (empty($this->options['not']) ? '>=' : '<');
$inner_filter->condition($this->real_field, $values[0], $is_range ? $range_op : $operator);
if ($is_range) {
$range_op = (empty($this->options['not']) ? '<=' : '>');
$inner_filter->condition($this->real_field, $values[1], $range_op);
$value_filter->filter($inner_filter);
}
$outer_filter->filter($value_filter);
}

$this->query->filter($outer_filter);
}
}
}

/**
* Converts a value to a timestamp, if it isn't one already.
*
* @param string|int $value
* The value to convert. Either a timestamp, or a date/time string as
* recognized by strtotime().
*
* @return int|false
* The parsed timestamp, or FALSE if an illegal string was passed.
*/
public function getTimestamp($value) {
if (is_numeric($value)) {
return $value;
}

return strtotime($value);
}

/**
* Fills $this->value with data from the argument.
*/
protected function fillValue() {
if (!empty($this->options['break_phrase'])) {
// Set up defaults:
if (!isset($this->value)) {
$this->value = array();
}

if (!isset($this->operator)) {
$this->operator = 'OR';
}

if (empty($this->argument)) {
return;
}

if (preg_match('/^([-\d;:\s]+\+)*[-\d;:\s]+$/', $this->argument)) {
// The '+' character in a query string may be parsed as ' '.
$this->value = explode('+', $this->argument);
}
elseif (preg_match('/^([-\d;:\s]+,)*[-\d;:\s]+$/', $this->argument)) {
$this->operator = 'AND';
$this->value = explode(',', $this->argument);
}

// Keep an 'error' value if invalid strings were given.
if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) {
$this->value = FALSE;
}
}
else {
$this->value = array($this->argument);
}
}

/**
* Aborts the associated query due to an illegal argument.
*/
protected function abort() {
$variables['!field'] = $this->definition['group'] . ': ' . $this->definition['title'];
$this->query->abort(t('Illegal argument passed to !field contextual filter.', $variables));
}

/**
* Computes the title this argument will assign the view, given the argument.
*
* @return string
* A title fitting for the passed argument.
*/
public function title() {
if (!empty($this->argument)) {
if (empty($this->value)) {
$this->fillValue();
}
$dates = array();
foreach ($this->value as $date) {
$date_parts = explode(';', $date);

$ts = $this->getTimestamp($date_parts[0]);
$datestr = format_date($ts, 'short');
if (count($date_parts) > 1) {
$ts = $this->getTimestamp($date_parts[1]);
$datestr .= ' - ' . format_date($ts, 'short');
}

if ($datestr) {
$dates[] = $datestr;
}
}

return $dates ? implode(', ', $dates) : check_plain($this->argument);
}

return check_plain($this->argument);
}

}
13 changes: 12 additions & 1 deletion contrib/search_api_views/includes/handler_argument_fulltext.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerArgumentFulltext.
*/

/**
* Views argument handler class for handling fulltext fields.
*/
@@ -61,7 +66,13 @@ class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumen
*/
public function query($group_by = FALSE) {
if ($this->options['fields']) {
$this->query->fields($this->options['fields']);
try {
$this->query->fields($this->options['fields']);
}
catch (SearchApiException $e) {
$this->query->abort($e->getMessage());
return;
}
}
if ($this->options['conjunction'] != 'AND') {
$this->query->setOption('conjunction', $this->options['conjunction']);
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerArgumentMoreLikeThis.
*/

/**
* Views argument handler providing a list of related items for search servers
* supporting the "search_api_mlt" feature.
@@ -13,6 +18,7 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
$options = parent::option_definition();
unset($options['break_phrase']);
unset($options['not']);
$options['entity_type'] = array('default' => FALSE);
$options['fields'] = array('default' => array());
return $options;
}
@@ -26,6 +32,20 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
unset($form['not']);

$index = search_api_index_load(substr($this->table, 17));

if ($index->datasource() instanceof SearchApiCombinedEntityDataSourceController) {
$types = array_intersect_key(search_api_entity_type_options_list(), array_flip($index->options['datasource']['types']));
$form['entity_type'] = array(
'#type' => 'select',
'#title' => t('Entity type'),
'#description' => t('Select the entity type of the argument.'),
'#options' => $types,
'#default_value' => $this->options['entity_type'],
'#required' => TRUE,
);
}


if (!empty($index->options['fields'])) {
$fields = array();
foreach ($index->getFields() as $key => $field) {
@@ -57,24 +77,38 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
* The argument sent may be found at $this->argument.
*/
public function query($group_by = FALSE) {
$server = $this->query->getIndex()->server();
if (!$server->supportsFeature('search_api_mlt')) {
$class = search_api_get_service_info($server->class);
watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
try {
$server = $this->query->getIndex()->server();
if (!$server->supportsFeature('search_api_mlt')) {
$class = search_api_get_service_info($server->class);
watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
array('@class' => $class['name']), WATCHDOG_ERROR);
$this->query->abort();
return;
}
$fields = $this->options['fields'] ? $this->options['fields'] : array();
if (empty($fields)) {
foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
$fields[] = $key;
$this->query->abort();
return;
}
$index_fields = array_keys($this->query->getIndex()->options['fields']);
if (empty($this->options['fields'])) {
$fields = $index_fields;
}
else {
$fields = array_intersect($this->options['fields'], $index_fields);
}
if ($this->query->getIndex()->datasource() instanceof SearchApiCombinedEntityDataSourceController) {
$id = $this->options['entity_type'] . '/' . $this->argument;
}
else {
$id = $this->argument;
}

$mlt = array(
'id' => $id,
'fields' => $fields,
);
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
}
catch (SearchApiException $e) {
$this->query->abort($e->getMessage());
}
$mlt = array(
'id' => $this->argument,
'fields' => $fields,
);
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
}

}
5 changes: 5 additions & 0 deletions contrib/search_api_views/includes/handler_argument_string.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerArgumentString.
*/

/**
* Views argument handler class for handling string fields.
*/
Original file line number Diff line number Diff line change
@@ -20,34 +20,42 @@ class SearchApiViewsHandlerArgumentTaxonomyTerm extends SearchApiViewsHandlerArg
$this->fillValue();
}

$outer_conjunction = strtoupper($this->operator);

if (empty($this->options['not'])) {
$operator = '=';
$conjunction = 'OR';
$inner_conjunction = 'OR';
}
else {
$operator = '<>';
$conjunction = 'AND';
$inner_conjunction = 'AND';
}

if (!empty($this->value)) {
$terms = entity_load('taxonomy_term', $this->value);
$vocabularies = taxonomy_vocabulary_get_names();

if (!empty($terms)) {
$filter = $this->query->createFilter($conjunction);
$filter = $this->query->createFilter($outer_conjunction);
$vocabulary_fields = $this->definition['vocabulary_fields'];
$vocabulary_fields += array('' => array());
foreach ($terms as $term) {
$inner_filter = $filter;
if ($outer_conjunction != $inner_conjunction) {
$inner_filter = $this->query->createFilter($inner_conjunction);
}
// Set filters for all term reference fields which don't specify a
// vocabulary, as well as for all fields specifying the term's
// vocabulary.
if (!empty($this->definition['vocabulary_fields'][$term->vocabulary_machine_name])) {
foreach ($this->definition['vocabulary_fields'][$term->vocabulary_machine_name] as $field) {
$filter->condition($field, $term->tid, $operator);
$inner_filter->condition($field, $term->tid, $operator);
}
}
foreach ($vocabulary_fields[''] as $field) {
$filter->condition($field, $term->tid, $operator);
$inner_filter->condition($field, $term->tid, $operator);
}
if ($outer_conjunction != $inner_conjunction) {
$filter->filter($inner_filter);
}
}

30 changes: 22 additions & 8 deletions contrib/search_api_views/includes/handler_filter.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilter.
*/

/**
* Views filter handler base class for handling all "normal" cases.
*/
@@ -31,8 +36,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
*/
public function operator_options() {
return array(
'<' => t('Is smaller than'),
'<=' => t('Is smaller than or equal to'),
'<' => t('Is less than'),
'<=' => t('Is less than or equal to'),
'=' => t('Is equal to'),
'<>' => t('Is not equal to'),
'>=' => t('Is greater than or equal to'),
@@ -46,8 +51,8 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
* Provide a form for setting the filter value.
*/
public function value_form(&$form, &$form_state) {
while (is_array($this->value)) {
$this->value = $this->value ? array_shift($this->value) : NULL;
while (is_array($this->value) && count($this->value) < 2) {
$this->value = $this->value ? reset($this->value) : NULL;
}
$form['value'] = array(
'#type' => 'textfield',
@@ -58,10 +63,19 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {

// Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
if (empty($form_state['exposed'])) {
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
elseif (!empty($this->options['expose']['use_operator'])) {
$name = $this->options['expose']['operator_id'];
$form['value']['#states']['visible'] = array(
':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
}

/**
5 changes: 5 additions & 0 deletions contrib/search_api_views/includes/handler_filter_boolean.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterBoolean.
*/

/**
* Views filter handler class for handling fulltext fields.
*/
94 changes: 85 additions & 9 deletions contrib/search_api_views/includes/handler_filter_date.inc
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
<?php

/**
* Views filter handler base class for handling all "normal" cases.
* @file
* Contains SearchApiViewsHandlerFilterDate.
*/
class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {

/**
* Views filter handler base class for handling date fields.
*/
class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilterNumeric {

/**
* Add a "widget type" option.
*/
public function option_definition() {
return parent::option_definition() + array(
'widget_type' => array('default' => 'default'),
'date_popup_format' => array('default' => 'm/d/Y'),
'year_range' => array('default' => '-3:+3'),
);
}

@@ -29,14 +36,49 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
*/
public function extra_options_form(&$form, &$form_state) {
parent::extra_options_form($form, $form_state);

if (module_exists('date_popup')) {
$widget_options = array('default' => 'Default', 'date_popup' => 'Date popup');
$widget_options = array(
'default' => 'Default',
'date_popup' => 'Date popup',
);
$form['widget_type'] = array(
'#type' => 'radios',
'#title' => t('Date selection form element'),
'#default_value' => $this->options['widget_type'],
'#options' => $widget_options,
);
$form['date_popup_format'] = array(
'#type' => 'textfield',
'#title' => t('Date format'),
'#default_value' => $this->options['date_popup_format'],
'#description' => t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "Y-m-d" or "m/d/Y".', array(
'@doc-link' => 'http://php.net/manual/en/function.date.php'
)),
'#states' => array(
'visible' => array(
':input[name="options[widget_type]"]' => array('value' => 'date_popup'),
),
),
);
}

if (module_exists('date_api')) {
$form['year_range'] = array(
'#type' => 'date_year_range',
'#default_value' => $this->options['year_range'],
);
}
}

/**
* Validate extra options.
*/
public function extra_options_validate($form, &$form_state) {
if (isset($form_state['values']['options']['year_range'])) {
if (!preg_match('/^(?:\-[0-9]{1,4}|[0-9]{4}):(?:[\+|\-][0-9]{1,4}|[0-9]{4})$/', $form_state['values']['options']['year_range'])) {
form_error($form['year_range'], t('Date year range must be in the format -9:+9, 2005:2010, -9:2010, or 2005:+9'));
}
}
}

@@ -46,11 +88,25 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);

$is_date_popup = ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup'));

// If the operator is between
if ($this->operator == 'between') {
if ($is_date_popup) {
$form['value']['min']['#type'] = 'date_popup';
$form['value']['min']['#date_format'] = $this->options['date_popup_format'];
$form['value']['min']['#date_year_range'] = $this->options['year_range'];
$form['value']['max']['#type'] = 'date_popup';
$form['value']['max']['#date_format'] = $this->options['date_popup_format'];
$form['value']['max']['#date_year_range'] = $this->options['year_range'];
}
}
// If we are using the date popup widget, overwrite the settings of the form
// according to what date_popup expects.
if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
elseif ($is_date_popup) {
$form['value']['#type'] = 'date_popup';
$form['value']['#date_format'] = 'm/d/Y';
$form['value']['#date_format'] = $this->options['date_popup_format'];
$form['value']['#date_year_range'] = $this->options['year_range'];
unset($form['value']['#description']);
}
elseif (empty($form_state['exposed'])) {
@@ -72,11 +128,31 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
elseif ($this->operator === 'not empty') {
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
}
else {
while (is_array($this->value)) {
$this->value = $this->value ? reset($this->value) : NULL;
elseif (in_array($this->operator, array('between', 'not between'), TRUE)) {
$min = isset($this->value[0]['min']) ? $this->value[0]['min'] : '';
if ($min !== '') {
$min = is_numeric($min) ? $min : strtotime($min, REQUEST_TIME);
}
$max = isset($this->value[0]['max']) ? $this->value[0]['max'] : '';
if ($max !== '') {
$max = is_numeric($max) ? $max : strtotime($max, REQUEST_TIME);
}
$v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);

if (is_numeric($min) && is_numeric($max)) {
$this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']);
}
elseif (is_numeric($min)) {
$operator = $this->operator === 'between' ? '>=' : '<';
$this->query->condition($this->real_field, $min, $operator, $this->options['group']);
}
elseif (is_numeric($max)) {
$operator = $this->operator === 'between' ? '<=' : '>';
$this->query->condition($this->real_field, $min, $operator, $this->options['group']);
}
}
else {
$value = isset($this->value[0]) ? $this->value[0]['value'] : $this->value['value'];
$v = is_numeric($value) ? $value : strtotime($value, REQUEST_TIME);
if ($v !== FALSE) {
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
}
207 changes: 207 additions & 0 deletions contrib/search_api_views/includes/handler_filter_entity.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterEntity.
*/

/**
* Views filter handler class for entities.
*
* Should be extended for specific entity types, such as
* SearchApiViewsHandlerFilterUser and SearchApiViewsHandlerFilterTaxonomyTerm.
*
* Based on views_handler_filter_term_node_tid.
*/
abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFilter {

/**
* If exposed form input was successfully validated, the entered entity IDs.
*
* @var array
*/
protected $validated_exposed_input;

/**
* Validates entered entity labels and converts them to entity IDs.
*
* Since this can come from either the form or the exposed filter, this is
* abstracted out a bit so it can handle the multiple input sources.
*
* @param array $form
* The form or form element for which any errors should be set.
* @param array $values
* The entered user names to validate.
*
* @return array
* The entity IDs corresponding to all entities that could be found.
*/
abstract protected function validate_entity_strings(array &$form, array $values);

/**
* Transforms an array of entity IDs into a comma-separated list of labels.
*
* @param array $ids
* The entity IDs to transform.
*
* @return string
* A string containing the labels corresponding to the IDs, separated by
* commas.
*/
abstract protected function ids_to_strings(array $ids);

/**
* {@inheritdoc}
*/
public function operator_options() {
$operators = array(
'=' => $this->isMultiValued() ? t('Is one of') : t('Is'),
'all of' => t('Is all of'),
'<>' => $this->isMultiValued() ? t('Is not one of') : t('Is not'),
'empty' => t('Is empty'),
'not empty' => t('Is not empty'),
);
if (!$this->isMultiValued()) {
unset($operators['all of']);
}
return $operators;
}

/**
* {@inheritdoc}
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);

if (!is_array($this->value)) {
$this->value = $this->value ? array($this->value) : array();
}

// Set the correct default value in case the admin-set value is used (and a
// value is present). The value is used if the form is either not exposed,
// or the exposed form wasn't submitted yet. (There doesn't seem to be an
// easier way to check for that.)
if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) {
$form['value']['#default_value'] = $this->ids_to_strings($this->value);
}
}

/**
* {@inheritdoc}
*/
public function value_validate($form, &$form_state) {
if (!empty($form['value'])) {
$value = &$form_state['values']['options']['value'];
if (strlen($value)) {
$values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
$ids = $this->validate_entity_strings($form['value'], $values);

if ($ids) {
$value = $ids;
}
}
}
}

/**
* {@inheritdoc}
*/
public function accept_exposed_input($input) {
$rc = parent::accept_exposed_input($input);

if ($rc) {
// If we have previously validated input, override.
if ($this->validated_exposed_input) {
$this->value = $this->validated_exposed_input;
}
}

return $rc;
}

/**
* {@inheritdoc}
*/
public function exposed_validate(&$form, &$form_state) {
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
return;
}

$this->validated_exposed_input = FALSE;
$identifier = $this->options['expose']['identifier'];
$input = $form_state['values'][$identifier];

if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
$this->operator = $this->options['group_info']['group_items'][$input]['operator'];
$input = $this->options['group_info']['group_items'][$input]['value'];
}

if (!strlen($input)) {
return;
}
$values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input);

if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) {
$this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values);
}
}

/**
* Determines whether multiple user names can be entered into this filter.
*
* This is either the case if the form isn't exposed, or if the " Allow
* multiple selections" option is enabled.
*
* @param array $options
* (optional) The options array to use. If not supplied, the options set on
* this filter will be used.
*
* @return bool
* TRUE if multiple values can be entered for this filter, FALSE otherwise.
*/
protected function isMultiValued(array $options = array()) {
$options = $options ? $options : $this->options;
return empty($options['exposed']) || !empty($options['expose']['multiple']);
}

/**
* {@inheritdoc}
*/
public function admin_summary() {
if (!is_array($this->value)) {
$this->value = $this->value ? array($this->value) : array();
}
$value = $this->value;
$this->value = empty($value) ? '' : $this->ids_to_strings($value);
$ret = parent::admin_summary();
$this->value = $value;
return $ret;
}

/**
* {@inheritdoc}
*/
public function query() {
if ($this->operator === 'empty') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
elseif ($this->operator === 'not empty') {
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
}
elseif (is_array($this->value)) {
$all_of = $this->operator === 'all of';
$operator = $all_of ? '=' : $this->operator;
if (count($this->value) == 1) {
$this->query->condition($this->real_field, reset($this->value), $operator, $this->options['group']);
}
else {
$filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR');
foreach ($this->value as $value) {
$filter->condition($this->real_field, $value, $operator);
}
$this->query->filter($filter, $this->options['group']);
}
}
}

}
135 changes: 121 additions & 14 deletions contrib/search_api_views/includes/handler_filter_fulltext.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterFulltext.
*/

/**
* Views filter handler class for handling fulltext fields.
*/
@@ -33,6 +38,7 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
$options['operator']['default'] = 'AND';

$options['mode'] = array('default' => 'keys');
$options['min_length'] = array('default' => '');
$options['fields'] = array('default' => array());

return $options;
@@ -75,6 +81,70 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
if (isset($form['expose'])) {
$form['expose']['#weight'] = -5;
}

$form['min_length'] = array(
'#title' => t('Minimum keyword length'),
'#description' => t('Minimum length of each word in the search keys. Leave empty to allow all words.'),
'#type' => 'textfield',
'#element_validate' => array('element_validate_integer_positive'),
'#default_value' => $this->options['min_length'],
);
}

/**
* {@inheritdoc}
*/
public function exposed_validate(&$form, &$form_state) {
// Only validate exposed input.
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
return;
}

// We only need to validate if there is a minimum word length set.
if ($this->options['min_length'] < 2) {
return;
}

$identifier = $this->options['expose']['identifier'];
$input = &$form_state['values'][$identifier];

if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
$this->operator = $this->options['group_info']['group_items'][$input]['operator'];
$input = &$this->options['group_info']['group_items'][$input]['value'];
}

// If there is no input, we're fine.
if (!trim($input)) {
return;
}

$words = preg_split('/\s+/', $input);
$quoted = FALSE;
foreach ($words as $i => $word) {
$word_length = drupal_strlen($word);
if (!$word_length) {
unset($words[$i]);
continue;
}
// Protect quoted strings.
if ($quoted && $word[strlen($word) - 1] === '"') {
$quoted = FALSE;
continue;
}
if ($quoted || $word[0] === '"') {
$quoted = TRUE;
continue;
}
if ($word_length < $this->options['min_length']) {
unset($words[$i]);
}
}
if (!$words) {
$vars['@count'] = $this->options['min_length'];
$msg = t('You must include at least one positive keyword with @count characters or more.', $vars);
form_error($form[$identifier], $msg);
}
$input = implode(' ', $words);
}

/**
@@ -89,7 +159,8 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
return;
}
$fields = $this->options['fields'];
$fields = $fields ? $fields : array_keys($this->getFulltextFields());
$available_fields = array_keys($this->getFulltextFields());
$fields = $fields ? array_intersect($fields, $available_fields) : $available_fields;

// If something already specifically set different fields, we silently fall
// back to mere filtering.
@@ -101,21 +172,29 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex

if ($filter) {
$filter = $this->query->createFilter('OR');
$op = $this->operator === 'NOT' ? '<>' : '=';
foreach ($fields as $field) {
$filter->condition($field, $this->value, $this->operator);
$filter->condition($field, $this->value, $op);
}
$this->query->filter($filter);
return;
}

// If the operator was set to OR, set it as the conjunction. (AND is set by
// default.)
if ($this->operator === 'OR') {
$this->query->setOption('conjunction', $this->operator);
// If the operator was set to OR or NOT, set OR as the conjunction. (It is
// also set for NOT since otherwise it would be "not all of these words".)
if ($this->operator != 'AND') {
$this->query->setOption('conjunction', 'OR');
}

$this->query->fields($fields);
$old = $this->query->getOriginalKeys();
try {
$this->query->fields($fields);
}
catch (SearchApiException $e) {
$this->query->abort($e->getMessage());
return;
}
$old = $this->query->getKeys();
$old_original = $this->query->getOriginalKeys();
$this->query->keys($this->value);
if ($this->operator == 'NOT') {
$keys = &$this->query->getKeys();
@@ -126,16 +205,44 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
// We can't know how negation is expressed in the server's syntax.
}
}

// If there were fulltext keys set, we take care to combine them in a
// meaningful way (especially with negated keys).
if ($old) {
$keys = &$this->query->getKeys();
// Array-valued keys are combined.
if (is_array($keys)) {
$keys[] = $old;
// If the old keys weren't parsed into an array, we instead have to
// combine the original keys.
if (is_scalar($old)) {
$keys = "($old) ({$this->value})";
}
else {
// If the conjunction or negation settings aren't the same, we have to
// nest both old and new keys array.
if (!empty($keys['#negation']) != !empty($old['#negation']) || $keys['#conjunction'] != $old['#conjunction']) {
$keys = array(
'#conjunction' => 'AND',
$old,
$keys,
);
}
// Otherwise, just add all individual words from the old keys to the
// new ones.
else {
foreach (element_children($old) as $i) {
$keys[] = $old[$i];
}
}
}
}
elseif (is_array($old)) {
// We don't support such nonsense.
}
else {
$keys = "($old) ($keys)";
// If the parse mode was "direct" for both old and new keys, we
// concatenate them and set them both via method and reference (to also
// update the originalKeys property.
elseif (is_scalar($old_original)) {
$combined_keys = "($old_original) ($keys)";
$this->query->keys($combined_keys);
$keys = $combined_keys;
}
}
}
38 changes: 19 additions & 19 deletions contrib/search_api_views/includes/handler_filter_language.inc
Original file line number Diff line number Diff line change
@@ -14,40 +14,40 @@
class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {

/**
* Provide a form for setting options.
* {@inheritdoc}
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);
$form['value']['#options'] = array(
'current' => t("Current user's language"),
'default' => t('Default site language'),
) + $form['value']['#options'];
}

/**
* Provides a summary of this filter's value for the admin UI.
*/
public function admin_summary() {
$tmp = $this->definition['options'];
$this->definition['options']['current'] = t('current');
$this->definition['options']['default'] = t('default');
$ret = parent::admin_summary();
$this->definition['options'] = $tmp;
return $ret;
protected function get_value_options() {
parent::get_value_options();
$options = array();
if (module_exists('language_hierarchy')) {
$options['fallback'] = t("Current user's language with fallback");
}
$options['current'] = t("Current user's language");
$options['default'] = t('Default site language');
$this->value_options = $options + $this->value_options;
}

/**
* Add this filter to the query.
*/
public function query() {
global $language_content;

if (!is_array($this->value)) {
$this->value = $this->value ? array($this->value) : array();
}
foreach ($this->value as $i => $v) {
if ($v == 'current') {
$this->value[$i] = $language_content->language;
}
elseif ($v == 'default') {
$this->value[$i] = language_default('language');
}
elseif ($v == 'fallback' && module_exists('language_hierarchy')) {
$fallbacks = array($language_content->language => $language_content->language);
$fallbacks += array_keys(language_hierarchy_get_ancestors($language_content->language));
$this->value[$i] = drupal_map_assoc($fallbacks);
}
}
parent::query();
}
209 changes: 209 additions & 0 deletions contrib/search_api_views/includes/handler_filter_numeric.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterNumeric.
*/

/**
* Views filter handler class for handling numeric and "string" fields.
*/
class SearchApiViewsHandlerFilterNumeric extends SearchApiViewsHandlerFilter {

/**
* {@inheritdoc}
*/
public function option_definition() {
$options = parent::option_definition();
$options['value'] = array(
'contains' => array(
'value' => array('default' => ''),
'min' => array('default' => ''),
'max' => array('default' => ''),
),
);

return $options;
}

/**
* {@inheritdoc}
*/
public function operator_options() {
$operators = parent::operator_options();

$index = search_api_index_load(substr($this->table, 17));
$server = NULL;
try {
if ($index) {
$server = $index->server();
}
}
catch (SearchApiException $e) {
// Ignore.
}
if ($server && $server->supportsFeature('search_api_between')) {
$operators += array(
'between' => t('Is between'),
'not between' => t('Is not between'),
);
}

return $operators;
}

/**
* Provides a form for setting the filter value.
*
* Heavily borrowed from views_handler_filter_numeric.
*
* @see views_handler_filter_numeric::value_form()
*/
public function value_form(&$form, &$form_state) {
$form['value']['#tree'] = TRUE;

$single_field_operators = $this->operator_options();
unset($single_field_operators['empty'], $single_field_operators['not empty'], $single_field_operators['between']);

// We have to make some choices when creating this as an exposed
// filter form. For example, if the operator is locked and thus
// not rendered, we can't render dependencies; instead we only
// render the form items we need.
$which = 'all';
if (!empty($form['operator'])) {
$source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator';
}

if (!empty($form_state['exposed'])) {
$identifier = $this->options['expose']['identifier'];
if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
// Exposed and locked.
$which = ($this->operator == 'between') ? 'minmax' : 'value';
}
else {
$source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']);
}
}

// Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
if ($which == 'all') {
$form['value']['value'] = array(
'#type' => 'textfield',
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#size' => 30,
'#default_value' => $this->value['value'],
'#dependency' => array($source => array_keys($single_field_operators)),
);
if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['value'])) {
$form_state['input'][$identifier]['value'] = $this->value['value'];
}
}
elseif ($which == 'value') {
// When exposed we drop the value-value and just do value if
// the operator is locked.
$form['value'] = array(
'#type' => 'textfield',
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#size' => 30,
'#default_value' => isset($this->value['value']) ? $this->value['value'] : '',
);
if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) {
$form_state['input'][$identifier] = isset($this->value['value']) ? $this->value['value'] : '';
}
}

if ($which == 'all' || $which == 'minmax') {
$form['value']['min'] = array(
'#type' => 'textfield',
'#title' => empty($form_state['exposed']) ? t('Min') : '',
'#size' => 30,
'#default_value' => $this->value['min'],
);
$form['value']['max'] = array(
'#type' => 'textfield',
'#title' => empty($form_state['exposed']) ? t('And max') : t('And'),
'#size' => 30,
'#default_value' => $this->value['max'],
);

if ($which == 'all') {
$form['value']['min']['#dependency'] = array($source => array('between'));
$form['value']['max']['#dependency'] = array($source => array('between'));
}

if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) {
$form_state['input'][$identifier]['min'] = $this->value['min'];
}
if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) {
$form_state['input'][$identifier]['max'] = $this->value['max'];
}

if (!isset($form['value']['value'])) {
// Ensure there is something in the 'value'.
$form['value']['value'] = array(
'#type' => 'value',
'#value' => NULL,
);
}
}
}

/**
* {@inheritdoc}
*/
public function admin_summary() {
if (!empty($this->options['exposed'])) {
return t('exposed');
}

if ($this->operator === 'empty') {
return t('is empty');
}
if ($this->operator === 'not empty') {
return t('is not empty');
}

$value = isset($this->value[0]) ? $this->value[0] : $this->value;

if (in_array($this->operator, array('between', 'not between'), TRUE)) {
// This is of course wrong for translation purposes, but copied from
// views_handler_filter_numeric::admin_summary() so probably still better
// to re-use this than to do it correctly.
$operator = $this->operator === 'between' ? t('between') : t('not between');
$vars = array(
'@min' => (string) $value['min'],
'@max' => (string) $value['max'],
);
return $operator . ' ' . t('@min and @max', $vars);
}

return check_plain((string) $this->operator) . ' ' . check_plain((string) $value['value']);

}

/**
* {@inheritdoc}
*/
public function query() {
if (in_array($this->operator, array('between', 'not between'), TRUE)) {
$min = isset($this->value[0]['min']) ? $this->value[0]['min'] : '';
$max = isset($this->value[0]['max']) ? $this->value[0]['max'] : '';
if ($min !== '' && $max !== '') {
$this->query->condition($this->real_field, array($min, $max), strtoupper($this->operator), $this->options['group']);
}
elseif ($min !== '') {
$operator = $this->operator === 'between' ? '>=' : '<';
$this->query->condition($this->real_field, $min, $operator, $this->options['group']);
}
elseif ($max !== '') {
$operator = $this->operator === 'between' ? '<=' : '>';
$this->query->condition($this->real_field, $min, $operator, $this->options['group']);
}
}
else {
parent::query();
}
}

}
164 changes: 132 additions & 32 deletions contrib/search_api_views/includes/handler_filter_options.inc
Original file line number Diff line number Diff line change
@@ -1,16 +1,82 @@
<?php

/**
* Views filter handler class for handling fields with a limited set of possible
* values.
*
* Definition items:
* - options: An array of possible values for this field.
* @file
* Contains the SearchApiViewsHandlerFilterOptions class.
*/

/**
* Views filter handler for fields with a limited set of possible values.
*/
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {

/**
* Stores the values which are available on the form.
*
* @var array
*/
protected $value_options = NULL;

/**
* The type of form element used to display the options.
*
* @var string
*/
protected $value_form_type = 'checkboxes';

/**
* Retrieves a wrapper for this filter's field.
*
* @return EntityMetadataWrapper|null
* A wrapper for the field which this filter uses.
*/
protected function get_wrapper() {
if ($this->query) {
$index = $this->query->getIndex();
}
elseif (substr($this->view->base_table, 0, 17) == 'search_api_index_') {
$index = search_api_index_load(substr($this->view->base_table, 17));
}
else {
return NULL;
}
$wrapper = $index->entityWrapper(NULL, TRUE);
$parts = explode(':', $this->real_field);
foreach ($parts as $i => $part) {
if (!isset($wrapper->$part)) {
return NULL;
}
$wrapper = $wrapper->$part;
$info = $wrapper->info();
if ($i < count($parts) - 1) {
// Unwrap lists.
$level = search_api_list_nesting_level($info['type']);
for ($j = 0; $j < $level; ++$j) {
$wrapper = $wrapper[0];
}
}
}

return $wrapper;
}

/**
* Fills the value_options property with all possible options.
*/
protected function get_value_options() {
if (isset($this->value_options)) {
return;
}

$wrapper = $this->get_wrapper();
if ($wrapper) {
$this->value_options = $wrapper->optionsList('view');
}
else {
$this->value_options = array();
}
}

/**
* Provide a list of options for the operator form.
*/
@@ -55,6 +121,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
*/
public function option_definition() {
$options = parent::option_definition();
$options['value'] = array('default' => '');
$options['expose']['contains']['reduce'] = array('default' => FALSE);
return $options;
}
@@ -63,13 +130,12 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
* Reduce the options according to the selection.
*/
protected function reduce_value_options() {
$options = array();
foreach ($this->definition['options'] as $id => $option) {
if (isset($this->options['value'][$id])) {
$options[$id] = $option;
foreach ($this->value_options as $id => $option) {
if (!isset($this->options['value'][$id])) {
unset($this->value_options[$id]);
}
}
return $options;
return $this->value_options;
}

/**
@@ -92,27 +158,38 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
* Provide a form for setting options.
*/
public function value_form(&$form, &$form_state) {
$options = array();
$this->get_value_options();
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
$options += $this->reduce_value_options($form_state);
$options = $this->reduce_value_options();
}
else {
$options += $this->definition['options'];
$options = $this->value_options;
}

$form['value'] = array(
'#type' => $this->value_form_type,
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#options' => $options,
'#multiple' => TRUE,
'#size' => min(4, count($this->definition['options'])),
'#size' => min(4, count($options)),
'#default_value' => is_array($this->value) ? $this->value : array(),
);
// Hide the value box if operator is 'empty' or 'not empty'.

// Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
if (empty($form_state['exposed'])) {
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
elseif (!empty($this->options['expose']['use_operator'])) {
$name = $this->options['expose']['operator_id'];
$form['value']['#states']['visible'] = array(
':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
}

/**
@@ -139,8 +216,9 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$values = '';

// Remove every element which is not known.
$this->get_value_options();
foreach ($this->value as $i => $value) {
if (!isset($this->definition['options'][$value])) {
if (!isset($this->value_options[$value])) {
unset($this->value[$i]);
}
}
@@ -161,7 +239,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
}
// If there is only a single value, use just the plain operator, = or <>.
$operator = check_plain($operator);
$values = check_plain($this->definition['options'][reset($this->value)]);
$values = check_plain($this->value_options[reset($this->value)]);
}
else {
foreach ($this->value as $value) {
@@ -172,13 +250,39 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$values .= '';
break;
}
$values .= check_plain($this->definition['options'][$value]);
$values .= check_plain($this->value_options[$value]);
}
}

return $operator . (($values !== '') ? ' ' . $values : '');
}

/**
* {@inheritdoc}
*/
function accept_exposed_input($input) {
$accepted = parent::accept_exposed_input($input);

// Grouped filters will have the raw form values structure from the
// checkboxes as the value here. Convert that into the correct array of
// values instead.
if ($accepted && is_array($this->value) && $this->is_a_group()) {
// For some reason, Views thinks it's a good idea to nest the form values
// into a second array in some cases. That one will be numerically indexed
// with just a single entry, though, so it should be relatively easy to
// spot.
if (count($this->value) && isset($this->value[0])) {
$this->value = reset($this->value);
}
$this->value = array_keys(array_filter($this->value));
if (!$this->value) {
return FALSE;
}
}

return $accepted;
}

/**
* Add this filter to the query.
*/
@@ -197,28 +301,24 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
$this->value = reset($this->value);
}

// Determine operator and conjunction.
// Determine operator and conjunction. The defaults are already right for
// "all of".
$operator = '=';
$conjunction = 'AND';
switch ($this->operator) {
case '=':
$operator = '=';
$conjunction = 'OR';
break;

case 'all of':
$operator = '=';
$conjunction = 'AND';
break;

case '<>':
$operator = '<>';
$conjunction = 'AND';
break;
}

// If the value is an empty array, we either want no filter at all (for
// "is none of", or want to find only items with no value for the field.
// "is none of"), or want to find only items with no value for the field.
if ($this->value === array()) {
if ($this->operator != '<>') {
if ($operator != '<>') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
return;
335 changes: 335 additions & 0 deletions contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterTaxonomyTerm.
*/

/**
* Views filter handler class for taxonomy term entities.
*
* Based on views_handler_filter_term_node_tid.
*/
class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilterEntity {

/**
* {@inheritdoc}
*/
public function has_extra_options() {
return !empty($this->definition['vocabulary']);
}

/**
* {@inheritdoc}
*/
public function option_definition() {
$options = parent::option_definition();

$options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select');
$options['hierarchy'] = array('default' => 0);
$options['expose']['contains']['reduce'] = array('default' => FALSE);
$options['error_message'] = array('default' => TRUE, 'bool' => TRUE);

return $options;
}

/**
* {@inheritdoc}
*/
public function extra_options_form(&$form, &$form_state) {
$form['type'] = array(
'#type' => 'radios',
'#title' => t('Selection type'),
'#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')),
'#default_value' => $this->options['type'],
);

$form['hierarchy'] = array(
'#type' => 'checkbox',
'#title' => t('Show hierarchy in dropdown'),
'#default_value' => !empty($this->options['hierarchy']),
);
$form['hierarchy']['#states']['visible'][':input[name="options[type]"]']['value'] = 'select';
}

/**
* {@inheritdoc}
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);

if (!empty($this->definition['vocabulary'])) {
$vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']);
$title = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name));
}
else {
$vocabulary = FALSE;
$title = t('Select terms');
}
$form['value']['#title'] = $title;

if ($vocabulary && $this->options['type'] == 'textfield') {
$form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid;
}
else {
if ($vocabulary && !empty($this->options['hierarchy'])) {
$tree = taxonomy_get_tree($vocabulary->vid, 0, NULL, TRUE);
$options = array();

if ($tree) {
foreach ($tree as $term) {
$choice = new stdClass();
$choice->option = array($term->tid => str_repeat('-', $term->depth) . check_plain(entity_label('taxonomy_term', $term)));
$options[] = $choice;
}
}
}
else {
$options = array();
$query = db_select('taxonomy_term_data', 'td');
$query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
$query->fields('td');
$query->orderby('tv.weight');
$query->orderby('tv.name');
$query->orderby('td.weight');
$query->orderby('td.name');
$query->addTag('taxonomy_term_access');
if ($vocabulary) {
$query->condition('tv.machine_name', $vocabulary->machine_name);
}
$result = $query->execute();
$tids = array();

foreach ($result as $term) {
$tids[] = $term->tid;
}
$terms = taxonomy_term_load_multiple($tids);

foreach ($terms as $term) {
$options[$term->tid] = check_plain(entity_label('taxonomy_term', $term));
}
}

$default_value = (array) $this->value;

if (!empty($form_state['exposed'])) {
$identifier = $this->options['expose']['identifier'];

if (!empty($this->options['expose']['reduce'])) {
$options = $this->reduce_value_options($options);

if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
$default_value = array();
}
}

if (empty($this->options['expose']['multiple'])) {
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
$default_value = 'All';
}
elseif (empty($default_value)) {
$keys = array_keys($options);
$default_value = array_shift($keys);
}
// Due to #1464174 there is a chance that array('') was saved in the
// admin ui. Let's choose a safe default value.
elseif ($default_value == array('')) {
$default_value = 'All';
}
else {
$copy = $default_value;
$default_value = array_shift($copy);
}
}
}
$form['value']['#type'] = 'select';
$form['value']['#multiple'] = TRUE;
$form['value']['#options'] = $options;
$form['value']['#size'] = min(9, count($options));
$form['value']['#default_value'] = $default_value;

if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) {
$form_state['input'][$identifier] = $default_value;
}
}
}

/**
* Reduces the available exposed options according to the selection.
*/
protected function reduce_value_options(array $options) {
foreach ($options as $id => $option) {
if (empty($this->options['value'][$id])) {
unset($options[$id]);
}
}
return $options;
}

/**
* {@inheritdoc}
*/
public function value_validate($form, &$form_state) {
// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
return;
}

parent::value_validate($form, $form_state);
}

/**
* {@inheritdoc}
*/
public function accept_exposed_input($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}

// We need to know the operator, which is normally set in
// views_handler_filter::accept_exposed_input(), before we actually call
// the parent version of ourselves.
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}

// If view is an attachment and is inheriting exposed filters, then assume
// exposed input has already been validated.
if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) {
$this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
}

// If we're checking for EMPTY or NOT, we don't need any input, and we can
// say that our input conditions are met by just having the right operator.
if ($this->operator == 'empty' || $this->operator == 'not empty') {
return TRUE;
}

// If it's non-required and there's no value don't bother filtering.
if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
return FALSE;
}

return parent::accept_exposed_input($input);
}

/**
* {@inheritdoc}
*/
public function exposed_validate(&$form, &$form_state) {
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
return;
}

// We only validate if they've chosen the text field style.
if ($this->options['type'] != 'textfield') {
$input = $form_state['values'][$this->options['expose']['identifier']];
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
$input = $this->options['group_info']['group_items'][$input]['value'];
}

if ($input != 'All') {
$this->validated_exposed_input = (array) $input;
}
return;
}

parent::exposed_validate($form, $form_state);
}

/**
* {@inheritdoc}
*/
public function expose_options() {
parent::expose_options();
$this->options['expose']['reduce'] = FALSE;
}

/**
* {@inheritdoc}
*/
protected function validate_entity_strings(array &$form, array $values) {
if (empty($values)) {
return array();
}

$tids = array();
$names = array();
$missing = array();
foreach ($values as $value) {
$missing[strtolower($value)] = TRUE;
$names[] = $value;
}

if (!$names) {
return FALSE;
}

$query = db_select('taxonomy_term_data', 'td');
$query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
$query->fields('td');
$query->condition('td.name', $names);
if (!empty($this->definition['vocabulary'])) {
$query->condition('tv.machine_name', $this->definition['vocabulary']);
}
$query->addTag('taxonomy_term_access');
$result = $query->execute();
foreach ($result as $term) {
unset($missing[strtolower($term->name)]);
$tids[] = $term->tid;
}

if ($missing) {
if (!empty($this->options['error_message'])) {
form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing)))));
}
else {
// Add a bogus TID which will show an empty result for a positive filter
// and be ignored for an excluding one.
$tids[] = 0;
}
}

return $tids;
}

/**
* {@inheritdoc}
*/
public function expose_form(&$form, &$form_state) {
parent::expose_form($form, $form_state);

if ($this->options['type'] == 'select') {
$form['expose']['reduce'] = array(
'#type' => 'checkbox',
'#title' => t('Limit list to selected items'),
'#description' => t('If checked, the only items presented to the user will be the ones selected here.'),
'#default_value' => $this->options['expose']['reduce'],
);
}
else {
$form['error_message'] = array(
'#type' => 'checkbox',
'#title' => t('Display error message'),
'#description' => t('Display an error message if one of the entered terms could not be found.'),
'#default_value' => $this->options['error_message'],
);
}
}

/**
* {@inheritdoc}
*/
protected function ids_to_strings(array $ids) {
$ids = array_filter($ids);
if (!$ids) {
return '';
}
return implode(', ', db_select('taxonomy_term_data', 'td')
->fields('td', array('name'))
->condition('td.tid', $ids)
->execute()
->fetchCol());
}

}
53 changes: 53 additions & 0 deletions contrib/search_api_views/includes/handler_filter_text.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterText.
*/

/**
* Views filter handler class for handling fulltext fields.
*/
@@ -12,4 +17,52 @@ class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
return array('=' => t('contains'), '<>' => t("doesn't contain"));
}

/**
* Determines whether input from the exposed filters affects this filter.
*
* Overridden to not treat "All" differently.
*
* @param array $input
* The user input from the exposed filters.
*
* @return bool
* TRUE if the input should change the behavior of this filter.
*/
public function accept_exposed_input($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}

if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}

if (!empty($this->options['expose']['identifier'])) {
$value = $input[$this->options['expose']['identifier']];

// Various ways to check for the absence of non-required input.
if (empty($this->options['expose']['required'])) {
if (($this->operator == 'empty' || $this->operator == 'not empty') && $value === '') {
$value = ' ';
}

if (!empty($this->always_multiple) && $value === '') {
return FALSE;
}
}

if (isset($value)) {
$this->value = $value;
if (empty($this->always_multiple) && empty($this->options['expose']['multiple'])) {
$this->value = array($value);
}
}
else {
return FALSE;
}
}

return TRUE;
}

}
79 changes: 79 additions & 0 deletions contrib/search_api_views/includes/handler_filter_user.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerFilterUser.
*/

/**
* Views filter handler class for handling user entities.
*
* Based on views_handler_filter_user_name.
*/
class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity {

/**
* {@inheritdoc}
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);

// Set autocompletion.
$path = $this->isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete';
$form['value']['#autocomplete_path'] = $path;
}

/**
* {@inheritdoc}
*/
protected function ids_to_strings(array $ids) {
$names = array();
$args[':uids'] = array_filter($ids);
if ($args[':uids']) {
$result = db_query('SELECT uid, name FROM {users} u WHERE uid IN (:uids)', $args);
$result = $result->fetchAllKeyed();
}
foreach ($ids as $uid) {
if (!$uid) {
$names[] = variable_get('anonymous', t('Anonymous'));
}
elseif (isset($result[$uid])) {
$names[] = $result[$uid];
}
}
return implode(', ', $names);
}

/**
* {@inheritdoc}
*/
protected function validate_entity_strings(array &$form, array $values) {
$uids = array();
$missing = array();
foreach ($values as $value) {
if (drupal_strtolower($value) === drupal_strtolower(variable_get('anonymous', t('Anonymous')))) {
$uids[] = 0;
}
else {
$missing[strtolower($value)] = $value;
}
}

if (!$missing) {
return $uids;
}

$result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing)));
foreach ($result as $account) {
unset($missing[strtolower($account->name)]);
$uids[] = $account->uid;
}

if ($missing) {
form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing))));
}

return $uids;
}

}
22 changes: 21 additions & 1 deletion contrib/search_api_views/includes/handler_sort.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsHandlerSort.
*/

/**
* Class for sorting results according to a specified field.
*/
@@ -23,8 +28,23 @@ class SearchApiViewsHandlerSort extends views_handler_sort {
unset($this->query->orderby);
$sort = &$this->query->getSort();
$sort = array();
unset($sort);
}

// If two of the same fields are used for sort, ignore the latter in order
// for the prior to take precedence. (Temporary workaround until
// https://www.drupal.org/node/2145547 is fixed in Views.)
$alreadySorted = $this->query->getSort();
if (is_array($alreadySorted) && isset($alreadySorted[$this->real_field])) {
return;
}

try {
$this->query->sort($this->real_field, $this->options['order']);
}
catch (SearchApiException $e) {
$this->query->abort($e->getMessage());
}
$this->query->sort($this->real_field, $this->options['order']);
}

}
25 changes: 19 additions & 6 deletions contrib/search_api_views/includes/plugin_cache.inc
Original file line number Diff line number Diff line change
@@ -77,31 +77,44 @@ class SearchApiViewsCache extends views_plugin_cache_time {
}

/**
* Overrides views_plugin_cache::get_results_key().
* Overrides views_plugin_cache::get_cache_key().
*
* Use the Search API query as the main source for the key.
* Use the Search API query as the main source for the key. Note that in
* Views < 3.8, this function does not exist.
*/
public function get_results_key() {
public function get_cache_key($key_data = array()) {
global $user;

if (!isset($this->_results_key)) {
$query = $this->getSearchApiQuery();
$query->preExecute();
$key_data = array(
$key_data += array(
'query' => $query,
'roles' => array_keys($user->roles),
'super-user' => $user->uid == 1, // special caching for super user.
'language' => $GLOBALS['language']->language,
'base_url' => $GLOBALS['base_url'],
'offset' => $this->view->get_current_page() . '*' . $this->view->get_items_per_page() . '+' . $this->view->get_offset(),
);
// Not sure what gets passed in exposed_info, so better include it. All
// other parameters used in the parent method are already reflected in the
// Search API query object we use.
if (isset($_GET['exposed_info'])) {
$key_data[$key] = $_GET[$key];
$key_data['exposed_info'] = $_GET['exposed_info'];
}
}
$key = drupal_hash_base64(serialize($key_data));
return $key;
}

$this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));
/**
* Overrides views_plugin_cache::get_results_key().
*
* This is unnecessary for Views >= 3.8.
*/
public function get_results_key() {
if (!isset($this->_results_key)) {
$this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . $this->get_cache_key();
}

return $this->_results_key;
203 changes: 151 additions & 52 deletions contrib/search_api_views/includes/query.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiViewsQuery.
*/

/**
* Views query class using a Search API index as the data source.
*/
@@ -117,17 +122,63 @@ class SearchApiViewsQuery extends views_plugin_query {
}

/**
* Add a sort to the query.
* Adds a sort to the query.
*
* @param $selector
* @param string $selector
* The field to sort on. All indexed fields of the index are valid values.
* In addition, the special fields 'search_api_relevance' (sort by
* relevance) and 'search_api_id' (sort by item id) may be used.
* @param $order
* In addition, these special fields may be used:
* - search_api_relevance: sort by relevance;
* - search_api_id: sort by item id;
* - search_api_random: random sort (available only if the server supports
* the "search_api_random_sort" feature).
* @param string $order
* The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
*/
public function add_selector_orderby($selector, $order = 'ASC') {
$this->query->sort($selector, $order);
if (!$this->errors) {
$this->query->sort($selector, $order);
}
}

/**
* Provides a sorting method as present in the Views default query plugin.
*
* This is provided so that the "Global: Random" sort included in Views will
* work properly with Search API Views. Random sorting is only supported if
* the active search server supports the "search_api_random_sort" feature,
* though, otherwise the call will be ignored.
*
* This method can only be used to sort randomly, as would be done with the
* default query plugin. All other calls are ignored.
*
* @param string|null $table
* Only "rand" is recognized here, all other calls are ignored.
* @param string|null $field
* Is ignored and only present for compatibility reasons.
* @param string $order
* Either "ASC" or "DESC".
* @param string|null $alias
* Is ignored and only present for compatibility reasons.
* @param array $params
* The following optional parameters are recognized:
* - seed: a predefined seed for the random generator.
*
* @see views_plugin_query_default::add_orderby()
*/
public function add_orderby($table, $field = NULL, $order = 'ASC', $alias = '', $params = array()) {
$server = $this->getIndex()->server();
if ($table == 'rand') {
if ($server->supportsFeature('search_api_random_sort')) {
$this->add_selector_orderby('search_api_random', $order);
if ($params) {
$this->setOption('search_api_random_sort', $params);
}
}
else {
$variables['%server'] = $server->label();
watchdog('search_api_views', 'Tried to sort results randomly on server %server which does not support random sorting.', $variables, WATCHDOG_WARNING);
}
}
}

/**
@@ -164,7 +215,7 @@ class SearchApiViewsQuery extends views_plugin_query {
'#default_value' => $this->options['search_api_bypass_access'],
);

if (entity_get_info($this->index->item_type)) {
if ($this->index && $this->index->getEntityType()) {
$form['entity_access'] = array(
'#type' => 'checkbox',
'#title' => t('Additional access checks on result entities'),
@@ -180,7 +231,6 @@ class SearchApiViewsQuery extends views_plugin_query {
'#options' => array(),
'#default_value' => $this->options['parse_mode'],
);
$modes = array();
foreach ($this->query->parseModes() as $key => $mode) {
$form['parse_mode']['#options'][$key] = $mode['name'];
if (!empty($mode['description'])) {
@@ -199,6 +249,10 @@ class SearchApiViewsQuery extends views_plugin_query {
* Builds the necessary info to execute the query.
*/
public function build(&$view) {
if (!empty($this->errors)) {
return;
}

$this->view = $view;

// Setup the nested filter structure for this query.
@@ -243,16 +297,6 @@ class SearchApiViewsQuery extends views_plugin_query {
$view->init_pager();
$this->pager->query();

// Views passes sometimes NULL and sometimes the integer 0 for "All" in a
// pager. If set to 0 items, a string "0" is passed. Therefore, we unset
// the limit if an empty value OTHER than a string "0" was passed.
if (!$this->limit && $this->limit !== '0') {
$this->limit = NULL;
}
// Set the range. (We always set this, as there might even be an offset if
// all items are shown.)
$this->query->range($this->offset, $this->limit);

// Set the search ID, if it was not already set.
if ($this->query->getOption('search id') == get_class($this->query)) {
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
@@ -262,6 +306,23 @@ class SearchApiViewsQuery extends views_plugin_query {
if (!empty($this->options['search_api_bypass_access'])) {
$this->query->setOption('search_api_bypass_access', TRUE);
}

// If the View and the Panel conspire to provide an overridden path then
// pass that through as the base path.
if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) {
$this->query->setOption('search_api_base_path', $this->view->override_path);
}

// Save query information for Views UI.
$view->build_info['query'] = (string) $this->query;
}

/**
* {@inheritdoc}
*/
public function alter(&$view) {
parent::alter($view);
drupal_alter('search_api_views_query', $view, $this);
}

/**
@@ -284,31 +345,61 @@ class SearchApiViewsQuery extends views_plugin_query {
return;
}

// Calculate the "skip result count" option, if it wasn't already set to
// FALSE.
$skip_result_count = $this->query->getOption('skip result count', TRUE);
if ($skip_result_count) {
$skip_result_count = !$this->pager || (!$this->pager->use_count_query() && empty($view->get_total_rows));
$this->query->setOption('skip result count', $skip_result_count);
}

try {
// Trigger pager pre_execute().
if ($this->pager) {
$this->pager->pre_execute($this->query);
}

// Views passes sometimes NULL and sometimes the integer 0 for "All" in a
// pager. If set to 0 items, a string "0" is passed. Therefore, we unset
// the limit if an empty value OTHER than a string "0" was passed.
if (!$this->limit && $this->limit !== '0') {
$this->limit = NULL;
}
// Set the range. (We always set this, as there might even be an offset if
// all items are shown.)
$this->query->range($this->offset, $this->limit);

$start = microtime(TRUE);

// Execute the search.
$results = $this->query->execute();
$this->search_api_results = $results;

// Store the results.
$this->pager->total_items = $view->total_rows = $results['result count'];
if (!empty($this->pager->options['offset'])) {
$this->pager->total_items -= $this->pager->options['offset'];
if (!$skip_result_count) {
$this->pager->total_items = $view->total_rows = $results['result count'];
if (!empty($this->pager->options['offset'])) {
$this->pager->total_items -= $this->pager->options['offset'];
}
$this->pager->update_page_info();
}
$this->pager->update_page_info();
$view->result = array();
if (!empty($results['results'])) {
$this->addResults($results['results'], $view);
}
// We shouldn't use $results['performance']['complete'] here, since
// extracting the results probably takes considerable time as well.
$view->execute_time = microtime(TRUE) - $start;

// Trigger pager post_execute().
if ($this->pager) {
$this->pager->post_execute($view->result);
}
}
catch (Exception $e) {
$this->errors[] = $e->getMessage();
// Recursion to get the same error behaviour as above.
return $this->execute($view);
$this->execute($view);
}
}

@@ -317,8 +408,14 @@ class SearchApiViewsQuery extends views_plugin_query {
*
* Used by handlers to flag a fatal error which shouldn't be displayed but
* still lead to the view returning empty and the search not being executed.
*
* @param string|null $msg
* Optionally, a translated, unescaped error message to display.
*/
public function abort() {
public function abort($msg = NULL) {
if ($msg) {
$this->errors[] = $msg;
}
$this->abort = TRUE;
}

@@ -334,9 +431,9 @@ class SearchApiViewsQuery extends views_plugin_query {
// First off, we try to gather as much field values as possible without
// loading any items.
foreach ($results as $id => $result) {
if (!empty($this->options['entity_access'])) {
$entity = entity_load($this->index->item_type, array($id));
if (!entity_access('view', $this->index->item_type, $entity[$id])) {
if (!empty($this->options['entity_access']) && ($entity_type = $this->index->getEntityType())) {
$entity = $this->index->loadItems(array($id));
if (!$entity || !entity_access('view', $entity_type, reset($entity))) {
continue;
}
}
@@ -356,11 +453,11 @@ class SearchApiViewsQuery extends views_plugin_query {

// Gather any fields from the search results.
if (!empty($result['fields'])) {
$row['_entity_properties'] += $result['fields'];
$row['_entity_properties'] += search_api_get_sanitized_field_values($result['fields']);
}

// Check whether we need to extract any properties from the result item.
$missing_fields = array_diff_key($this->fields, $row);
$missing_fields = array_diff_key($this->fields, $row['_entity_properties']);
if ($missing_fields) {
$missing[$id] = $missing_fields;
if (is_object($row['entity'])) {
@@ -378,14 +475,14 @@ class SearchApiViewsQuery extends views_plugin_query {
// Load items of those rows which haven't got all field values, yet.
if (!empty($ids)) {
$items += $this->index->loadItems($ids);
// $items now includes loaded items, and those already passed in the
// search results.
foreach ($items as $id => $item) {
// Extract item properties.
$wrapper = $this->index->entityWrapper($item, FALSE);
$rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
$rows[$id]->entity = $item;
}
}
// $items now includes all loaded items from which fields still need to be
// extracted.
foreach ($items as $id => $item) {
// Extract item properties.
$wrapper = $this->index->entityWrapper($item, FALSE);
$rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
$rows[$id]->entity = $item;
}

// Finally, add all rows to the Views result set.
@@ -450,31 +547,31 @@ class SearchApiViewsQuery extends views_plugin_query {
* query backend.
*/
public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
$entity_type = $this->index->getEntityType();
$type = $this->index->getEntityType() ? $this->index->getEntityType() : $this->index->item_type;
$wrappers = array();
$load_entities = array();
$load_items = array();
foreach ($results as $row_index => $row) {
if ($entity_type && isset($row->entity)) {
if (isset($row->entity)) {
// If this entity isn't load, register it for pre-loading.
if (!is_object($row->entity)) {
$load_entities[$row->entity] = $row_index;
$load_items[$row->entity] = $row_index;
}
else {
$wrappers[$row_index] = $this->index->entityWrapper($row->entity);
}

$wrappers[$row_index] = $this->index->entityWrapper($row->entity);
}
}

// If the results are entities, we pre-load them to make use of a multiple
// load. (Otherwise, each result would be loaded individually.)
if (!empty($load_entities)) {
$entities = entity_load($entity_type, array_keys($load_entities));
foreach ($entities as $entity_id => $entity) {
$wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
if (!empty($load_items)) {
$items = $this->index->loadItems(array_keys($load_items));
foreach ($items as $id => $item) {
$wrappers[$load_items[$id]] = $this->index->entityWrapper($item);
}
}

// Apply the relationship, if necessary.
$type = $entity_type ? $entity_type : $this->index->item_type;
$selector_suffix = '';
if ($field && ($pos = strrpos($field, ':'))) {
$selector_suffix = substr($field, 0, $pos);
@@ -520,9 +617,9 @@ class SearchApiViewsQuery extends views_plugin_query {
// Query interface methods (proxy to $this->query)
//

public function createFilter($conjunction = 'AND') {
public function createFilter($conjunction = 'AND', $tags = array()) {
if (!$this->errors) {
return $this->query->createFilter($conjunction);
return $this->query->createFilter($conjunction, $tags);
}
}

@@ -620,16 +717,18 @@ class SearchApiViewsQuery extends views_plugin_query {
return $ret;
}

public function getOption($name) {
public function getOption($name, $default = NULL) {
if (!$this->errors) {
return $this->query->getOption($name);
return $this->query->getOption($name, $default);
}
return $default;
}

public function setOption($name, $value) {
if (!$this->errors) {
return $this->query->setOption($name, $value);
}
return NULL;
}

public function &getOptions() {
34 changes: 34 additions & 0 deletions contrib/search_api_views/search_api_views.api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* @file
* Hooks provided by the Search Views module.
*/

/**
* Alter the query before executing the query.
*
* @param view $view
* The view object about to be processed.
* @param SearchApiViewsQuery $query
* The Search API Views query to be altered.
*
* @see hook_views_query_alter()
*/
function hook_search_api_views_query_alter(view &$view, SearchApiViewsQuery &$query) {
// (Example assuming a view with an exposed filter on node title.)
// If the input for the title filter is a positive integer, filter against
// node ID instead of node title.
if ($view->name == 'my_view' && is_numeric($view->exposed_raw_input['title']) && $view->exposed_raw_input['title'] > 0) {
// Traverse through the 'where' part of the query.
foreach ($query->where as &$condition_group) {
foreach ($condition_group['conditions'] as &$condition) {
// If this is the part of the query filtering on title, chang the
// condition to filter on node ID.
if (reset($condition) == 'node.title') {
$condition = array('node.nid', $view->exposed_raw_input['title'],'=');
}
}
}
}
}
6 changes: 5 additions & 1 deletion contrib/search_api_views/search_api_views.info
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name = Search views
description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
dependencies[] = search_api
@@ -12,14 +11,19 @@ files[] = includes/handler_argument.inc
files[] = includes/handler_argument_fulltext.inc
files[] = includes/handler_argument_more_like_this.inc
files[] = includes/handler_argument_string.inc
files[] = includes/handler_argument_date.inc
files[] = includes/handler_argument_taxonomy_term.inc
files[] = includes/handler_filter.inc
files[] = includes/handler_filter_boolean.inc
files[] = includes/handler_filter_date.inc
files[] = includes/handler_filter_entity.inc
files[] = includes/handler_filter_fulltext.inc
files[] = includes/handler_filter_language.inc
files[] = includes/handler_filter_numeric.inc
files[] = includes/handler_filter_options.inc
files[] = includes/handler_filter_taxonomy_term.inc
files[] = includes/handler_filter_text.inc
files[] = includes/handler_filter_user.inc
files[] = includes/handler_sort.inc
files[] = includes/plugin_cache.inc
files[] = includes/query.inc
16 changes: 12 additions & 4 deletions contrib/search_api_views/search_api_views.install
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/**
* @file
* Install, update and uninstall functions for the search_api_views module.
@@ -24,15 +25,15 @@ function search_api_views_update_7101() {
if (!$table_fields) {
return;
}
foreach (views_get_all_views() as $name => $view) {
foreach (views_get_all_views() as $view) {
if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
continue;
}
$change = FALSE;
$fields = $table_fields[$view->base_table];
$change |= _search_api_views_update_7101_helper($view->base_field, $fields);
if (!empty($view->display)) {
foreach ($view->display as $key => &$display) {
foreach ($view->display as &$display) {
$options = &$display->display_options;
if (isset($options['style_options']['grouping'])) {
$change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
@@ -66,8 +67,15 @@ function search_api_views_update_7101() {
/**
* Helper function for replacing field identifiers.
*
* @return
* TRUE iff the identifier was changed.
* @param $field
* Some data to be searched for field names that should be altered. Passed by
* reference.
* @param array $fields
* An array mapping Search API field identifiers (as previously used by Views)
* to the new, sanitized Views field identifiers.
*
* @return bool
* TRUE if any data was changed, FALSE otherwise.
*/
function _search_api_views_update_7101_helper(&$field, array $fields) {
if (is_array($field)) {
29 changes: 26 additions & 3 deletions contrib/search_api_views/search_api_views.module
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Integrates the Search API with Views.
*/

/**
* Implements hook_views_api().
*/
@@ -12,7 +17,7 @@ function search_api_views_views_api() {
/**
* Implements hook_search_api_index_insert().
*/
function search_api_views_search_api_index_insert(SearchApiIndex $index) {
function search_api_views_search_api_index_insert() {
// Make the new index available for views.
views_invalidate_cache();
}
@@ -21,16 +26,34 @@ function search_api_views_search_api_index_insert(SearchApiIndex $index) {
* Implements hook_search_api_index_update().
*/
function search_api_views_search_api_index_update(SearchApiIndex $index) {
if (!$index->enabled && $index->original->enabled) {
// Check whether index was disabled.
$is_enabled = $index->enabled;
$was_enabled = $index->original->enabled;
if (!$is_enabled && $was_enabled) {
_search_api_views_index_unavailable($index);
return;
}

// Check whether the indexed fields changed.
$old_fields = $index->original->options + array('fields' => array());
$old_fields = $old_fields['fields'];
$new_fields = $index->options + array('fields' => array());
$new_fields = $new_fields['fields'];

// If the index was enabled or its fields changed, invalidate the Views cache.
if ($is_enabled != $was_enabled || $old_fields != $new_fields) {
views_invalidate_cache();
}
}

/**
* Implements hook_search_api_index_delete().
*/
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
_search_api_views_index_unavailable($index);
// Only do this if this is a "real" deletion, no revert.
if (!$index->hasStatus(ENTITY_IN_CODE)) {
_search_api_views_index_unavailable($index);
}
}

/**
110 changes: 94 additions & 16 deletions contrib/search_api_views/search_api_views.views.inc
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<?php

/**
* @file
* Views hook implementations for the Search API Views module.
*/

/**
* Implements hook_views_data().
*/
function search_api_views_views_data() {
try {
$data = array();
$entity_types = entity_get_info();
foreach (search_api_index_load_multiple(FALSE) as $index) {
// Fill in base data.
$key = 'search_api_index_' . $index->machine_name;
@@ -20,14 +24,16 @@ function search_api_views_views_data() {
'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
'query class' => 'search_api_views_query',
);
if (isset($entity_types[$index->getEntityType()])) {
$table['table'] += array(
'entity type' => $index->getEntityType(),
'skip entity load' => TRUE,
);
}
$table['table']['entity type'] = $index->getEntityType();
$table['table']['skip entity load'] = TRUE;

$wrapper = $index->entityWrapper(NULL, TRUE);
try {
$wrapper = $index->entityWrapper(NULL, FALSE);
}
catch (EntityMetadataWrapperException $e) {
watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
continue;
}

// Add field handlers and relationships provided by the Entity API.
foreach ($wrapper as $key => $property) {
@@ -37,6 +43,14 @@ function search_api_views_views_data() {
}
}

try {
$wrapper = $index->entityWrapper(NULL);
}
catch (EntityMetadataWrapperException $e) {
watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
continue;
}

// Add handlers for all indexed fields.
foreach ($index->getFields() as $key => $field) {
$tmp = $wrapper;
@@ -63,7 +77,7 @@ function search_api_views_views_data() {
if ($group) {
// @todo Entity type label instead of $group?
$table[$id]['group'] = $group;
$name = t('@field (indexed)', array('@field' => $name));
$name = t('!field (indexed)', array('!field' => $name));
}
$table[$id]['title'] = $name;
$table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
@@ -115,8 +129,8 @@ function search_api_views_views_data() {
if (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
$field_id = ($pos = strrpos($key, ':')) ? substr($key, $pos + 1) : $key;
$field_info = field_info_field($field_id);
if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
$vocabulary_fields[$field_info['settings']['allowed_values'][0]['vocabulary']][] = $key;
if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) {
$vocabulary_fields[$vocabulary][] = $key;
}
else {
$vocabulary_fields[''][] = $key;
@@ -139,8 +153,18 @@ function search_api_views_views_data() {
}

/**
* Helper function that returns an array of handler definitions to add to a
* views field definition.
* Adds handler definitions for a field to a Views data table definition.
*
* Helper method for search_api_views_views_data().
*
* @param $id
* The internal identifier of the field.
* @param array $field
* Information about the field.
* @param EntityMetadataWrapper $wrapper
* A wrapper providing further metadata about the field.
* @param array $table
* The existing Views data table definition, as a reference.
*/
function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) {
$type = $field['type'];
@@ -155,7 +179,7 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
if ($inner_type == 'text') {
$table[$id] += array(
'argument' => array(
'handler' => 'SearchApiViewsHandlerArgument',
'handler' => 'SearchApiViewsHandlerArgumentString',
),
'filter' => array(
'handler' => 'SearchApiViewsHandlerFilterText',
@@ -164,9 +188,9 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
return;
}

if ($options = $wrapper->optionsList('view')) {
$info = $wrapper->info();
if (isset($info['options list']) && is_callable($info['options list'])) {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
$table[$id]['filter']['options'] = $options;
$table[$id]['filter']['multi-valued'] = search_api_is_list_type($type);
}
elseif ($inner_type == 'boolean') {
@@ -175,13 +199,39 @@ function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper
elseif ($inner_type == 'date') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
}
elseif (isset($field['entity_type']) && $field['entity_type'] === 'user') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterUser';
}
elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm';
$field_info = field_info_field($info['name']);
// For the "Parent terms" and "All parent terms" properties, we can
// extrapolate the vocabulary from the parent in the selector. (E.g.,
// for "field_tags:parent" we can use the information of "field_tags".)
// Otherwise, we can't include any vocabulary information.
if (!$field_info && ($info['name'] == 'parent' || $info['name'] == 'parents_all')) {
if (!empty($table[$id]['real field'])) {
$parts = explode(':', $table[$id]['real field']);
$field_info = field_info_field($parts[count($parts) - 2]);
}
}
if ($vocabulary = _search_api_views_get_field_vocabulary($field_info)) {
$table[$id]['filter']['vocabulary'] = $vocabulary;
}
}
elseif (in_array($inner_type, array('integer', 'decimal', 'duration', 'string'))) {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterNumeric';
}
else {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
}

if ($inner_type == 'string' || $inner_type == 'uri') {
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentString';
}
elseif ($inner_type == 'date') {
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentDate';
}
else {
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument';
}
@@ -240,3 +290,31 @@ function search_api_views_views_plugins() {

return $ret;
}

/**
* Returns the vocabulary machine name of a term field.
*
* @param array|null $field_info
* The field's field info array, or NULL if the field is not provided by the
* Field API. See the return value of field_info_field().
*
* @return string|null
* If the field contains taxonomy terms of a single vocabulary (which could be
* determined), that vocabulary's machine name; NULL otherwise.
*/
function _search_api_views_get_field_vocabulary($field_info) {
// Test for "Term reference" fields.
if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
return $field_info['settings']['allowed_values'][0]['vocabulary'];
}
// Test for "Entity reference" fields.
elseif (isset($field_info['settings']['handler']) && $field_info['settings']['handler'] === 'base') {
if (!empty($field_info['settings']['handler_settings']['target_bundles'])) {
$bundles = $field_info['settings']['handler_settings']['target_bundles'];
if (count($bundles) == 1) {
return key($bundles);
}
}
}
return NULL;
}
17 changes: 16 additions & 1 deletion includes/callback.inc
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ interface SearchApiAlterCallbackInterface {
/**
* Check whether this data-alter callback is applicable for a certain index.
*
* This can be used for hiding the callback on the index's "Workflow" tab. To
* This can be used for hiding the callback on the index's "Filters" tab. To
* avoid confusion, you should only use criteria that are immutable, such as
* the index's entity type. Also, since this is only used for UI purposes, you
* should not completely rely on this to ensure certain index configurations
@@ -182,4 +182,19 @@ abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackI
return array();
}

/**
* Determines whether the given index contains multiple types of entities.
*
* @param SearchApiIndex|null $index
* (optional) The index to examine. Defaults to the index set for this
* plugin.
*
* @return bool
* TRUE if the index is a multi-entity index, FALSE otherwise.
*/
protected function isMultiEntityIndex(SearchApiIndex $index = NULL) {
$index = $index ? $index : $this->index;
return $index->datasource() instanceof SearchApiCombinedEntityDataSourceController;
}

}
97 changes: 87 additions & 10 deletions includes/callback_add_aggregation.inc
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
<?php

/**
* @file
* Contains SearchApiAlterAddAggregation.
*/

/**
* Search API data alteration callback that adds an URL field for all items.
*/
class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {

/**
* The type of aggregation currently performed.
*
* Used to temporarily store the current aggregation type for use of
* SearchApiAlterAddAggregation::reduce() with array_reduce().
*
* @var string
*/
protected $reductionType;

/**
* A separator to use when the aggregation type is 'fulltext'.
*
* Used to temporarily store a string separator when the aggregation type is
* "fulltext", for use in SearchApiAlterAddAggregation::reduce() with
* array_reduce().
*
* @var string
*/
protected $fulltextReductionSeparator;

public function configurationForm() {
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';

$fields = $this->index->getFields(FALSE);
$field_options = array();
$field_properties = array();
foreach ($fields as $name => $field) {
$field_options[$name] = $field['name'];
$field_options[$name] = check_plain($field['name']);
$field_properties[$name] = array(
'#attributes' => array('title' => $name),
'#description' => check_plain($field['description']),
);
}
$additional = empty($this->options['fields']) ? array() : $this->options['fields'];

@@ -60,17 +91,31 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'#required' => TRUE,
);
$form['fields'][$name]['type_descriptions'] = $type_descriptions;
$type_selector = ':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]';
foreach (array_keys($types) as $type) {
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][$type_selector]['value'] = $type;
}
$form['fields'][$name]['fields'] = array(
$form['fields'][$name]['separator'] = array(
'#type' => 'textfield',
'#title' => t('Fulltext separator'),
'#description' => t('For aggregation type "Fulltext", set the text that should be used to separate the aggregated field values. Use "\t" for tabs and "\n" for newline characters.'),
'#default_value' => addcslashes(isset($field['separator']) ? $field['separator'] : "\n\n", "\0..\37\\"),
'#states' => array(
'visible' => array(
$type_selector => array(
'value' => 'fulltext',
),
),
),
);
$form['fields'][$name]['fields'] = array_merge($field_properties, array(
'#type' => 'checkboxes',
'#title' => t('Contained fields'),
'#options' => $field_options,
'#default_value' => drupal_map_assoc($field['fields']),
'#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')),
'#required' => TRUE,
);
));
$form['fields'][$name]['actions'] = array(
'#type' => 'actions',
'remove' => array(
@@ -106,11 +151,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return;
}
foreach ($values['fields'] as $name => $field) {
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
unset($values['fields'][$name]['actions']);
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
if ($field['name'] && !$fields) {
form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.'));
}
$values['fields'][$name]['separator'] = stripcslashes($field['separator']);
}
}

@@ -157,6 +203,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
$values = $this->flattenArray($values);

$this->reductionType = $field['type'];
$this->fulltextReductionSeparator = isset($field['separator']) ? $field['separator'] : "\n\n";
$item->$name = array_reduce($values, array($this, 'reduce'), NULL);
if ($field['type'] == 'count' && !$item->$name) {
$item->$name = 0;
@@ -173,7 +220,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
public function reduce($a, $b) {
switch ($this->reductionType) {
case 'fulltext':
return isset($a) ? $a . "\n\n" . $b : $b;
return isset($a) ? $a . $this->fulltextReductionSeparator . $b : $b;
case 'sum':
return $a + $b;
case 'count':
@@ -184,7 +231,22 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
return isset($a) ? min($a, $b) : $b;
case 'first':
return isset($a) ? $a : $b;
case 'first_char':
$b = "$b";
if (isset($a) || $b === '') {
return $a;
}
return drupal_substr($b, 0, 1);
case 'last':
return isset($b) ? $b : $a;
case 'list':
if (!isset($a)) {
$a = array();
}
$a[] = $b;
return $a;
}
return NULL;
}

/**
@@ -237,10 +299,13 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
/**
* Helper method for getting all available aggregation types.
*
* @param $info (optional)
* One of "name", "type" or "description", to indicate what values should be
* returned for the types. Defaults to "name".
* @param string $info
* (optional) One of "name", "type" or "description", to indicate what
* information should be returned for the types.
*
* @return string[]
* An associative array of aggregation type identifiers mapped to their
* names, data types or descriptions, as requested.
*/
protected function getTypes($info = 'name') {
switch ($info) {
@@ -252,6 +317,9 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'max' => t('Maximum'),
'min' => t('Minimum'),
'first' => t('First'),
'first_char' => t('First letter'),
'last' => t('Last'),
'list' => t('List'),
);
case 'type':
return array(
@@ -260,7 +328,10 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'count' => 'integer',
'max' => 'integer',
'min' => 'integer',
'first' => 'string',
'first' => 'token',
'first_char' => 'token',
'last' => 'token',
'list' => 'list<token>',
);
case 'description':
return array(
@@ -270,8 +341,12 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'max' => t('The Maximum aggregation computes the numerically largest contained field value.'),
'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'),
'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'),
'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'),
'last' => t('The Last aggregation will simply keep the last encountered field value.'),
'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
);
}
return array();
}

/**
@@ -280,6 +355,8 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
public function formButtonSubmit(array $form, array &$form_state) {
$button_name = $form_state['triggering_element']['#name'];
if ($button_name == 'op') {
// Increment $i until the corresponding field is not set, then create the
// field with that number as suffix.
for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
}
$this->options['fields']['search_api_aggregation_' . $i] = array(
66 changes: 17 additions & 49 deletions includes/callback_add_hierarchy.inc
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<?php

/**
* Search API data alteration callback that adds an URL field for all items.
* @file
* Contains SearchApiAlterAddHierarchy.
*/

/**
* Adds all ancestors for hierarchical fields.
*/
class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {

@@ -15,24 +20,16 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
protected $field_options;

/**
* Enable this data alteration only if any hierarchical fields are available.
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* @param SearchApiIndex $index
* The index to check for.
*
* @return boolean
* TRUE if the callback can run on the given index; FALSE otherwise.
* Returns TRUE only if any hierarchical fields are available.
*/
public function supportsIndex(SearchApiIndex $index) {
return (bool) $this->getHierarchicalFields();
}

/**
* Display a form for configuring this callback.
*
* @return array
* A form array for configuring this callback, or FALSE if no configuration
* is possible.
* {@inheritdoc}
*/
public function configurationForm() {
$options = $this->getHierarchicalFields();
@@ -54,35 +51,23 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}

/**
* Submit callback for the form returned by configurationForm().
*
* This method should both return the new options and set them internally.
*
* @param array $form
* The form returned by configurationForm().
* @param array $values
* The part of the $form_state['values'] array corresponding to this form.
* @param array $form_state
* The complete form state.
*
* @return array
* The new options array for this callback.
* {@inheritdoc}
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
// Change the saved type of fields in the index, if necessary.
if (!empty($this->index->options['fields'])) {
$fields = &$this->index->options['fields'];
$previous = drupal_map_assoc($this->options['fields']);
foreach ($values['fields'] as $field) {
list($key, $prop) = explode(':', $field);
list($key) = explode(':', $field);
if (empty($previous[$field]) && isset($fields[$key]['type'])) {
$fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>';
$change = TRUE;
}
}
$new = drupal_map_assoc($values['fields']);
foreach ($previous as $field) {
list($key, $prop) = explode(':', $field);
list($key) = explode(':', $field);
if (empty($new[$field]) && isset($fields[$key]['type'])) {
$w = $this->index->entityWrapper(NULL, FALSE);
if (isset($w->$key)) {
@@ -102,19 +87,11 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}

/**
* Alter items before indexing.
*
* Items which are removed from the array won't be indexed, but will be marked
* as clean for future indexing. This could for instance be used to implement
* some sort of access filter for security purposes (e.g., don't index
* unpublished nodes or comments).
*
* @param array $items
* An array of items to be altered, keyed by item IDs.
* {@inheritdoc}
*/
public function alterItems(array &$items) {
if (empty($this->options['fields'])) {
return array();
return;
}
foreach ($items as $item) {
$wrapper = $this->index->entityWrapper($item, FALSE);
@@ -131,22 +108,13 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
$this->extractHierarchy($child, $prop, $values[$key]);
}
foreach ($values as $key => $value) {
$item->$key = $value;
$item->$key = array_values($value);
}
}
}

/**
* Declare the properties that are (or can be) added to items with this
* callback. If a property with this name already exists for an entity it
* will be overridden, so keep a clear namespace by prefixing the properties
* with the module name if this is not desired.
*
* @see hook_entity_property_info()
*
* @return array
* Information about all additional properties, as specified by
* hook_entity_property_info() (only the inner "properties" array).
* {@inheritdoc}
*/
public function propertyInfo() {
if (empty($this->options['fields'])) {
@@ -188,7 +156,7 @@ class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
}

/**
* Helper method for finding all hierarchical fields of an index's type.
* Finds all hierarchical fields for the current index.
*
* @return array
* An array containing all hierarchical fields of the index, structured as
7 changes: 6 additions & 1 deletion includes/callback_add_url.inc
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<?php

/**
* @file
* Contains SearchApiAlterAddUrl.
*/

/**
* Search API data alteration callback that adds an URL field for all items.
*/
class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback {

public function alterItems(array &$items) {
foreach ($items as $id => &$item) {
foreach ($items as &$item) {
$url = $this->index->datasource()->getItemUrl($item);
if (!$url) {
$item->search_api_url = NULL;
9 changes: 7 additions & 2 deletions includes/callback_add_viewed_entity.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiAlterAddViewedEntity.
*/

/**
* Search API data alteration callback that adds an URL field for all items.
*/
@@ -64,12 +69,12 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {

$type = $this->index->getEntityType();
$mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
foreach ($items as $id => &$item) {
foreach ($items as &$item) {
// Since we can't really know what happens in entity_view() and render(),
// we use try/catch. This will at least prevent some errors, even though
// it's no protection against fatal errors and the like.
try {
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode);
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode, $item->search_api_language);
$text = render($render);
if (!$text) {
$item->search_api_viewed = NULL;
129 changes: 96 additions & 33 deletions includes/callback_bundle_filter.inc
Original file line number Diff line number Diff line change
@@ -1,55 +1,113 @@
<?php

/**
* Search API data alteration callback that filters out items based on their
* bundle.
* @file
* Contains SearchApiAlterBundleFilter.
*/

/**
* Represents a data alteration that restricts entity indexes to some bundles.
*/
class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {

/**
* {@inheritdoc}
*/
public function supportsIndex(SearchApiIndex $index) {
if ($this->isMultiEntityIndex($index)) {
$info = entity_get_info();
foreach ($index->options['datasource']['types'] as $type) {
if (isset($info[$type]) && self::hasBundles($info[$type])) {
return TRUE;
}
}
return FALSE;
}
return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info);
}

/**
* {@inheritdoc}
*/
public function alterItems(array &$items) {
$info = entity_get_info($this->index->getEntityType());
if (self::hasBundles($info) && isset($this->options['bundles'])) {
$bundles = array_flip($this->options['bundles']);
$default = (bool) $this->options['default'];
if (!$this->supportsIndex($this->index) || !isset($this->options['bundles'])) {
return;
}

$multi_entity = $this->isMultiEntityIndex();
if ($multi_entity) {
$bundle_prop = 'item_bundle';
}
else {
$info = entity_get_info($this->index->getEntityType());
$bundle_prop = $info['entity keys']['bundle'];
foreach ($items as $id => $item) {
if (isset($bundles[$item->$bundle_prop]) == $default) {
unset($items[$id]);
}
}

$bundles = array_flip($this->options['bundles']);
$default = (bool) $this->options['default'];

foreach ($items as $id => $item) {
// Ignore types that have no bundles.
if ($multi_entity && !self::hasBundles(entity_get_info($item->item_type))) {
continue;
}
if (isset($bundles[$item->$bundle_prop]) == $default) {
unset($items[$id]);
}
}
}

/**
* {@inheritdoc}
*/
public function configurationForm() {
$info = entity_get_info($this->index->getEntityType());
if (self::hasBundles($info)) {
if ($this->supportsIndex($this->index)) {
$options = array();
foreach ($info['bundles'] as $bundle => $bundle_info) {
$options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
if ($this->isMultiEntityIndex()) {
$info = entity_get_info();
$unsupported_types = array();
foreach ($this->index->options['datasource']['types'] as $type) {
if (isset($info[$type]) && self::hasBundles($info[$type])) {
foreach ($info[$type]['bundles'] as $bundle => $bundle_info) {
$options["$type:$bundle"] = $info[$type]['label'] . ' » ' . $bundle_info['label'];
}
}
else {
$unsupported_types[] = isset($info[$type]['label']) ? $info[$type]['label'] : $type;
}
}
if ($unsupported_types) {
$form['unsupported_types']['#markup'] = '<p>' . t('The following entity types do not contain any bundles: @types. All items of those types will therefore be included in the index.', array('@types' => implode(', ', $unsupported_types))) . '</p>';
}
}
$form = array(
'default' => array(
'#type' => 'radios',
'#title' => t('Which items should be indexed?'),
'#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
'#options' => array(
1 => t('All but those from one of the selected bundles'),
0 => t('Only those from the selected bundles'),
),
),
'bundles' => array(
'#type' => 'select',
'#title' => t('Bundles'),
'#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(),
'#options' => $options,
'#size' => min(4, count($options)),
'#multiple' => TRUE,
else {
$info = entity_get_info($this->index->getEntityType());
foreach ($info['bundles'] as $bundle => $bundle_info) {
$options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
}
}
if (!empty($this->index->options['datasource']['bundles'])) {
$form['message']['#markup'] = '<p>' . t("<strong>Note:</strong> This index is already restricted to certain bundles. If you use this data alteration, those will be reduced further. However, the index setting is better supported in the user interface and should therefore be prefered. For example, using this data alteration will not reduce the displayed total number of items to index (even though some of them will not be indexed). Consider creating a new index with appropriate bundle settings instead.") . '</p>';
$included_bundles = array_flip($this->index->options['datasource']['bundles']);
$options = array_intersect_key($options, $included_bundles);
}
$form['default'] = array(
'#type' => 'radios',
'#title' => t('Which items should be indexed?'),
'#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
'#options' => array(
1 => t('All but those from one of the selected bundles'),
0 => t('Only those from the selected bundles'),
),
);
$form['bundles'] = array(
'#type' => 'select',
'#title' => t('Bundles'),
'#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(),
'#options' => $options,
'#size' => min(4, count($options)),
'#multiple' => TRUE,
);
}
else {
$form = array(
@@ -62,8 +120,13 @@ class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
}

/**
* Helper method for figuring out if the entities with the given entity info
* can be filtered by bundle.
* Determines whether a certain entity type has any bundles.
*
* @param array $entity_info
* The entity type's entity_get_info() array.
*
* @return bool
* TRUE if the entity type has bundles, FALSE otherwise.
*/
protected static function hasBundles(array $entity_info) {
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
46 changes: 46 additions & 0 deletions includes/callback_comment_access.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains the SearchApiAlterCommentAccess class.
*/

/**
* Adds node access information to comment indexes.
*/
class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess {

/**
* Overrides SearchApiAlterNodeAccess::supportsIndex().
*
* Returns TRUE only for indexes on comments.
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->getEntityType() === 'comment';
}

/**
* Overrides SearchApiAlterNodeAccess::getNode().
*
* Returns the comment's node, instead of the item (i.e., the comment) itself.
*/
protected function getNode($item) {
return node_load($item->nid);
}

/**
* Overrides SearchApiAlterNodeAccess::configurationFormSubmit().
*
* Doesn't index the comment's "Author".
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
$old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_comment_access']['status']);
$new_status = !empty($form_state['values']['callbacks']['search_api_alter_comment_access']['status']);

if (!$old_status && $new_status) {
$form_state['index']->options['fields']['status']['type'] = 'boolean';
}

return parent::configurationFormSubmit($form, $values, $form_state);
}

}
51 changes: 11 additions & 40 deletions includes/callback_language_control.inc
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?php

/**
* @file
* Contains SearchApiAlterLanguageControl.
*/

/**
* Search API data alteration callback that filters out items based on their
* bundle.
*/
class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {

/**
* Construct a data-alter callback.
*
* @param SearchApiIndex $index
* The index whose items will be altered.
* @param array $options
* The callback options set for this index.
* {@inheritdoc}
*/
public function __construct(SearchApiIndex $index, array $options = array()) {
$options += array(
@@ -23,27 +23,18 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
}

/**
* Check whether this data-alter callback is applicable for a certain index.
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* Only returns TRUE if the system is multilingual.
*
* @param SearchApiIndex $index
* The index to check for.
*
* @return boolean
* TRUE if the callback can run on the given index; FALSE otherwise.
*
* @see drupal_multilingual()
*/
public function supportsIndex(SearchApiIndex $index) {
return drupal_multilingual();
}

/**
* Display a form for configuring this data alteration.
*
* @return array
* A form array for configuring this data alteration.
* {@inheritdoc}
*/
public function configurationForm() {
$form = array();
@@ -79,7 +70,7 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
foreach ($list as $lang) {
$name = t($lang->name);
$native = $lang->native;
$languages[$lang->language] = ($name == $native) ? $name : "$name ($native)";
$languages[$lang->language] = check_plain(($name == $native) ? $name : "$name ($native)");
if (!$lang->enabled) {
$languages[$lang->language] .= ' [' . t('disabled') . ']';
}
@@ -98,35 +89,15 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
}

/**
* Submit callback for the form returned by configurationForm().
*
* This method should both return the new options and set them internally.
*
* @param array $form
* The form returned by configurationForm().
* @param array $values
* The part of the $form_state['values'] array corresponding to this form.
* @param array $form_state
* The complete form state.
*
* @return array
* The new options array for this callback.
* {@inheritdoc}
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
$values['languages'] = array_filter($values['languages']);
return parent::configurationFormSubmit($form, $values, $form_state);
}

/**
* Alter items before indexing.
*
* Items which are removed from the array won't be indexed, but will be marked
* as clean for future indexing. This could for instance be used to implement
* some sort of access filter for security purposes (e.g., don't index
* unpublished nodes or comments).
*
* @param array $items
* An array of items to be altered, keyed by item IDs.
* {@inheritdoc}
*/
public function alterItems(array &$items) {
foreach ($items as $i => &$item) {
55 changes: 22 additions & 33 deletions includes/callback_node_access.inc
Original file line number Diff line number Diff line change
@@ -10,31 +10,19 @@
class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {

/**
* Check whether this data-alter callback is applicable for a certain index.
* Overrides SearchApiAbstractAlterCallback::supportsIndex().
*
* Returns TRUE only for indexes on nodes.
*
* @param SearchApiIndex $index
* The index to check for.
*
* @return boolean
* TRUE if the callback can run on the given index; FALSE otherwise.
*/
public function supportsIndex(SearchApiIndex $index) {
// Currently only node access is supported.
return $index->getEntityType() === 'node';
}

/**
* Declare the properties that are (or can be) added to items with this callback.
* Overrides SearchApiAbstractAlterCallback::propertyInfo().
*
* Adds the "search_api_access_node" property.
*
* @see hook_entity_property_info()
*
* @return array
* Information about all additional properties, as specified by
* hook_entity_property_info() (only the inner "properties" array).
*/
public function propertyInfo() {
return array(
@@ -47,15 +35,7 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
}

/**
* Alter items before indexing.
*
* Items which are removed from the array won't be indexed, but will be marked
* as clean for future indexing. This could for instance be used to implement
* some sort of access filter for security purposes (e.g., don't index
* unpublished nodes or comments).
*
* @param array $items
* An array of items to be altered, keyed by item IDs.
* {@inheritdoc}
*/
public function alterItems(array &$items) {
static $account;
@@ -65,30 +45,39 @@ class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
$account = drupal_anonymous_user();
}

foreach ($items as $nid => &$item) {
foreach ($items as $id => $item) {
$node = $this->getNode($item);
// Check whether all users have access to the node.
if (!node_access('view', $item, $account)) {
if (!node_access('view', $node, $account)) {
// Get node access grants.
$result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $item->nid));
$result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->nid));

// Store all grants together with it's realms in the item.
// Store all grants together with their realms in the item.
foreach ($result as $grant) {
if (!isset($items[$nid]->search_api_access_node)) {
$items[$nid]->search_api_access_node = array();
}
$items[$nid]->search_api_access_node[] = "node_access_$grant->realm:$grant->gid";
$items[$id]->search_api_access_node[] = "node_access_{$grant->realm}:{$grant->gid}";
}
}
else {
// Add the generic view grant if we are not using node access or the
// node is viewable by anonymous users.
$items[$nid]->search_api_access_node = array('node_access__all');
$items[$id]->search_api_access_node = array('node_access__all');
}
}
}

/**
* Submit callback for the configuration form.
* Retrieves the node related to a search item.
*
* In the default implementation for nodes, the item is already the node.
* Subclasses may override this to easily provide node access checks for
* items related to nodes.
*/
protected function getNode($item) {
return $item;
}

/**
* Overrides SearchApiAbstractAlterCallback::configurationFormSubmit().
*
* If the data alteration is being enabled, set "Published" and "Author" to
* "indexed", because both are needed for the node access filter.
19 changes: 16 additions & 3 deletions includes/callback_role_filter.inc
Original file line number Diff line number Diff line change
@@ -16,17 +16,30 @@ class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
* This plugin only supports indexes containing users.
*/
public function supportsIndex(SearchApiIndex $index) {
if ($this->isMultiEntityIndex($index)) {
return in_array('user', $index->options['datasource']['types']);
}
return $index->getEntityType() == 'user';
}

/**
* Implements SearchApiAlterCallbackInterface::alterItems().
*/
public function alterItems(array &$items) {
$roles = $this->options['roles'];
$selected_roles = $this->options['roles'];
$default = (bool) $this->options['default'];
foreach ($items as $id => $account) {
$role_match = (count(array_diff_key($account->roles, $roles)) !== count($account->roles));
$multi_types = $this->isMultiEntityIndex($this->index);
foreach ($items as $id => $item) {
if ($multi_types) {
if ($item->item_type !== 'user') {
continue;
}
$item_roles = $item->user->roles;
}
else {
$item_roles = $item->roles;
}
$role_match = (count(array_diff_key($item_roles, $selected_roles)) !== count($item_roles));
if ($role_match === $default) {
unset($items[$id]);
}
57 changes: 57 additions & 0 deletions includes/callback_user_content.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/**
* @file
* Contains SearchApiAlterAddUserContent.
*/

/**
* Adds the nodes created by the indexed user for indexing.
*/
class SearchApiAlterAddUserContent extends SearchApiAbstractAlterCallback {

/**
* {@inheritdoc}
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->getEntityType() === 'user';
}

/**
* {@inheritdoc}
*/
public function propertyInfo() {
return array(
'search_api_user_content' => array(
'label' => t('User content'),
'description' => t('The nodes created by this user'),
'type' => 'list<node>',
),
);
}

/**
* {@inheritdoc}
*/
public function alterItems(array &$items) {
$uids = array();
foreach ($items as $item) {
$uids[] = $item->uid;
}

$sql = 'SELECT nid, uid FROM {node} WHERE uid IN (:uids)';
$nids = db_query($sql, array(':uids' => $uids));
$user_nodes = array();
foreach ($nids as $row) {
$user_nodes[$row->uid][] = $row->nid;
}

foreach ($items as $item) {
$item->search_api_user_content = array();
if (!empty($user_nodes[$item->uid])) {
$item->search_api_user_content = $user_nodes[$item->uid];
}
}
}

}
31 changes: 31 additions & 0 deletions includes/callback_user_status.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/**
* @file
* Contains the SearchApiAlterUserStatus class.
*/

/**
* Filters out blocked user accounts.
*/
class SearchApiAlterUserStatus extends SearchApiAbstractAlterCallback {

/**
* {@inheritdoc}
*/
public function supportsIndex(SearchApiIndex $index) {
return $index->getEntityType() == 'user';
}

/**
* {@inheritdoc}
*/
public function alterItems(array &$items) {
foreach ($items as $id => $account) {
if (empty($account->status)) {
unset($items[$id]);
}
}
}

}
488 changes: 263 additions & 225 deletions includes/datasource.inc

Large diffs are not rendered by default.

336 changes: 250 additions & 86 deletions includes/datasource_entity.inc

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion includes/datasource_external.inc
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceC
* loadable, specify a function here.
*
* @param array $ids
* The IDs of the items to laod.
* The IDs of the items to load.
*
* @return array
* The loaded items, keyed by ID.
360 changes: 360 additions & 0 deletions includes/datasource_multiple.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
<?php

/**
* @file
* Contains SearchApiCombinedEntityDataSourceController.
*/

/**
* Provides a datasource for indexing multiple types of entities.
*/
class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataSourceController {

/**
* {@inheritdoc}
*/
protected $table = 'search_api_item_string_id';

/**
* {@inheritdoc}
*/
public function getIdFieldInfo() {
return array(
'key' => 'item_id',
'type' => 'string',
);
}

/**
* {@inheritdoc}
*/
public function loadItems(array $ids) {
$ids_by_type = array();
foreach ($ids as $id) {
list($type, $entity_id) = explode('/', $id);
$ids_by_type[$type][$entity_id] = $id;
}

$items = array();
foreach ($ids_by_type as $type => $type_ids) {
foreach (entity_load($type, array_keys($type_ids)) as $entity_id => $entity) {
$id = $type_ids[$entity_id];
$item = (object) array($type => $entity);
$item->item_id = $id;
$item->item_type = $type;
$item->item_entity_id = $entity_id;
$item->item_bundle = NULL;
// Add the item language so the "search_api_language" field will work
// correctly.
$item->language = isset($entity->language) ? $entity->language : NULL;
try {
list(, , $bundle) = entity_extract_ids($type, $entity);
$item->item_bundle = $bundle ? "$type:$bundle" : NULL;
}
catch (EntityMalformedException $e) {
// Will probably make problems at some other place, but for extracting
// the bundle it is really not critical enough to fail on – just
// ignore this exception.
}
$items[$id] = $item;
unset($type_ids[$entity_id]);
}
if ($type_ids) {
search_api_track_item_delete($type, array_keys($type_ids));
}
}

return $items;
}

/**
* {@inheritdoc}
*/
protected function getPropertyInfo() {
$info = array(
'item_id' => array(
'label' => t('ID'),
'description' => t('The combined ID of the item, containing both entity type and entity ID.'),
'type' => 'token',
),
'item_type' => array(
'label' => t('Entity type'),
'description' => t('The entity type of the item.'),
'type' => 'token',
'options list' => 'search_api_entity_type_options_list',
),
'item_entity_id' => array(
'label' => t('Entity ID'),
'description' => t('The entity ID of the item.'),
'type' => 'token',
),
'item_bundle' => array(
'label' => t('Bundle'),
'description' => t('The bundle of the item, if applicable.'),
'type' => 'token',
'options list' => 'search_api_combined_bundle_options_list',
),
'item_label' => array(
'label' => t('Label'),
'description' => t('The label of the item.'),
'type' => 'text',
// Since this needs a bit more computation than the others, we don't
// include it always when loading the item but use a getter callback.
'getter callback' => 'search_api_get_multi_type_item_label',
),
);

foreach ($this->getSelectedEntityTypeOptions() as $type => $label) {
$info[$type] = array(
'label' => $label,
'description' => t('The indexed entity, if it is of type %type.', array('%type' => $label)),
'type' => $type,
);
}

return array('property info' => $info);
}

/**
* {@inheritdoc}
*/
public function getItemId($item) {
return isset($item->item_id) ? $item->item_id : NULL;
}

/**
* {@inheritdoc}
*/
public function getItemLabel($item) {
return search_api_get_multi_type_item_label($item);
}

/**
* {@inheritdoc}
*/
public function getItemUrl($item) {
if ($item->item_type == 'file') {
return array(
'path' => file_create_url($item->file->uri),
'options' => array(
'entity_type' => 'file',
'entity' => $item,
),
);
}
$url = entity_uri($item->item_type, $item->{$item->item_type});
return $url ? $url : NULL;
}

/**
* {@inheritdoc}
*/
public function startTracking(array $indexes) {
if (!$this->table) {
return;
}
// We first clear the tracking table for all indexes, so we can just insert
// all items again without any key conflicts.
$this->stopTracking($indexes);

foreach ($indexes as $index) {
$types = $this->getEntityTypes($index);

// Wherever possible, use a sub-select instead of the much slower
// entity_load().
foreach ($types as $type) {
$entity_info = entity_get_info($type);

if (!empty($entity_info['base table'])) {
// Assumes that all entities use the "base table" property and the
// "entity keys[id]" in the same way as the default controller.
$id_field = $entity_info['entity keys']['id'];
$table = $entity_info['base table'];

// Select all entity ids.
$query = db_select($table, 't');
$query->addExpression("CONCAT(:prefix, t.$id_field)", 'item_id', array(':prefix' => $type . '/'));
$query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
$query->addExpression('1', 'changed');

// INSERT ... SELECT ...
db_insert($this->table)
->from($query)
->execute();

unset($types[$type]);
}
}

// In the absence of a "base table", use the slow entity_load().
if ($types) {
foreach ($types as $type) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', $type);
$result = $query->execute();
$ids = !empty($result[$type]) ? array_keys($result[$type]) : array();
if ($ids) {
foreach ($ids as $i => $id) {
$ids[$i] = $type . '/' . $id;
}
$this->trackItemInsert($ids, array($index), TRUE);
}
}
}
}
}

/**
* Starts tracking the index status for the given items on the given indexes.
*
* @param array $item_ids
* The IDs of new items to track.
* @param SearchApiIndex[] $indexes
* The indexes for which items should be tracked.
* @param bool $skip_type_check
* (optional) If TRUE, don't check whether the type matches the index's
* datasource configuration. Internal use only.
*
* @return SearchApiIndex[]|null
* All indexes for which any items were added; or NULL if items were added
* for all of them.
*
* @throws SearchApiDataSourceException
* If any error state was encountered.
*/
public function trackItemInsert(array $item_ids, array $indexes, $skip_type_check = FALSE) {
$ret = array();

foreach ($indexes as $index_id => $index) {
$ids = drupal_map_assoc($item_ids);

if (!$skip_type_check) {
$types = $this->getEntityTypes($index);
foreach ($ids as $id) {
list($type) = explode('/', $id);
if (!isset($types[$type])) {
unset($ids[$id]);
}
}
}

if ($ids) {
parent::trackItemInsert($ids, array($index));
$ret[$index_id] = $index;
}
}

return $ret;
}

/**
* {@inheritdoc}
*/
public function configurationForm(array $form, array &$form_state) {
$form['types'] = array(
'#type' => 'checkboxes',
'#title' => t('Entity types'),
'#description' => t('Select the entity types which should be included in this index.'),
'#options' => array_map('check_plain', search_api_entity_type_options_list()),
'#attributes' => array('class' => array('search-api-checkboxes-list')),
'#disabled' => !empty($form_state['index']),
'#required' => TRUE,
);
if (!empty($form_state['index']->options['datasource']['types'])) {
$form['types']['#default_value'] = $this->getEntityTypes($form_state['index']);
}
return $form;
}

/**
* {@inheritdoc}
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
if (!empty($values['types'])) {
$values['types'] = array_keys(array_filter($values['types']));
}
}

/**
* {@inheritdoc}
*/
public function getConfigurationSummary(SearchApiIndex $index) {
if ($type_labels = $this->getSelectedEntityTypeOptions($index)) {
$args['!types'] = implode(', ', $type_labels);
return format_plural(count($type_labels), 'Indexed entity types: !types.', 'Indexed entity types: !types.', $args);
}
return NULL;
}

/**
* Retrieves the index for which the current method was called.
*
* Very ugly method which uses the stack trace to find the right object.
*
* @return SearchApiIndex
* The active index.
*
* @throws SearchApiException
* Thrown if the active index could not be determined.
*/
protected function getCallingIndex() {
foreach (debug_backtrace() as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof SearchApiIndex) {
return $trace['object'];
}
}
// If there's only a single index on the site, it's also easy.
$indexes = search_api_index_load_multiple(FALSE);
if (count($indexes) === 1) {
return reset($indexes);
}
throw new SearchApiException('Could not determine the active index of the datasource.');
}

/**
* Returns the entity types for which this datasource is configured.
*
* Depends on the index from which this method is (indirectly) called.
*
* @param SearchApiIndex $index
* (optional) The index for which to get the enabled entity types. If not
* given, will be determined automatically.
*
* @return string[]
* The machine names of the datasource's enabled entity types, as both keys
* and values.
*
* @throws SearchApiException
* Thrown if the active index could not be determined.
*/
protected function getEntityTypes(SearchApiIndex $index = NULL) {
if (!$index) {
$index = $this->getCallingIndex();
}
if (isset($index->options['datasource']['types'])) {
return drupal_map_assoc($index->options['datasource']['types']);
}
return array();
}

/**
* Returns the selected entity type options for this datasource.
*
* Depends on the index from which this method is (indirectly) called.
*
* @param SearchApiIndex $index
* (optional) The index for which to get the enabled entity types. If not
* given, will be determined automatically.
*
* @return string[]
* An associative array, mapping the machine names of the enabled entity
* types to their labels.
*
* @throws SearchApiException
* Thrown if the active index could not be determined.
*/
protected function getSelectedEntityTypeOptions(SearchApiIndex $index = NULL) {
return array_intersect_key(search_api_entity_type_options_list(), $this->getEntityTypes($index));
}

}
7 changes: 6 additions & 1 deletion includes/exception.inc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php

/**
* @file
* Contains SearchApiException.
*/

/**
* Represents an exception or error that occurred in some part of the Search API
* framework.
@@ -14,7 +19,7 @@ class SearchApiException extends Exception {
*/
public function __construct($message = NULL) {
if (!$message) {
$message = t('An error occcurred in the Search API framework.');
$message = t('An error occurred in the Search API framework.');
}
parent::__construct($message);
}
Loading