Skip to content

Commit

Permalink
Merge pull request #9844 from colemanw/CRM-20034
Browse files Browse the repository at this point in the history
CRM-20034 - CRM-20034 - Support OR grouping of api get params
  • Loading branch information
colemanw authored Feb 19, 2017
2 parents cd8eca8 + 40875a9 commit 93f1eff
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 26 deletions.
41 changes: 28 additions & 13 deletions Civi/API/Api3SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Api3SelectQuery extends SelectQuery {
* @inheritDoc
*/
protected function buildWhereClause() {
$filters = array();
foreach ($this->where as $key => $value) {
$table_name = NULL;
$column_name = NULL;
Expand Down Expand Up @@ -104,23 +105,37 @@ protected function buildWhereClause() {
// Just ignore this for the $where_clause.
continue;
}
if (!is_array($value)) {
$this->query->where(array("`$table_name`.`$column_name` = @value"), array(
"@value" => $value,
));
$operator = is_array($value) ? \CRM_Utils_Array::first(array_keys($value)) : NULL;
if (!in_array($operator, \CRM_Core_DAO::acceptedSQLOperators())) {
$value = array('=' => $value);
}
else {
// We expect only one element in the array, of the form
// "operator" => "rhs".
$operator = \CRM_Utils_Array::first(array_keys($value));
if (!in_array($operator, \CRM_Core_DAO::acceptedSQLOperators())) {
$this->query->where(array("{$table_name}.{$column_name} = @value"), array("@value" => $value));
}
else {
$this->query->where(\CRM_Core_DAO::createSQLFilter("{$table_name}.{$column_name}", $value));
$filters[$key] = \CRM_Core_DAO::createSQLFilter("{$table_name}.{$column_name}", $value);
}
// Support OR groups
if (!empty($this->where['options']['or'])) {
$orGroups = $this->where['options']['or'];
if (is_string($orGroups)) {
$orGroups = array_map('trim', explode(',', $orGroups));
}
if (!is_array(\CRM_Utils_Array::first($orGroups))) {
$orGroups = array($orGroups);
}
foreach ($orGroups as $orGroup) {
$orClause = array();
foreach ($orGroup as $key) {
if (!isset($filters[$key])) {
throw new \CiviCRM_API3_Exception("'$key' specified in OR group but not added to params");
}
$orClause[] = $filters[$key];
unset($filters[$key]);
}
$this->query->where(implode(' OR ', $orClause));
}
}
// Add the remaining params using AND
foreach ($filters as $filter) {
$this->query->where($filter);
}
}

/**
Expand Down
42 changes: 39 additions & 3 deletions api/v3/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ function _civicrm_api3_activity_get_spec(&$params) {
'type' => 1,
'FKClassName' => 'CRM_Core_DAO_Tag',
'FKApiName' => 'Tag',
'supports_joins' => TRUE,
);
$params['file_id'] = array(
'title' => 'Attached Files',
'description' => 'Find activities with attached files.',
'type' => 1,
'FKClassName' => 'CRM_Core_DAO_File',
'FKApiName' => 'File',
);
$params['case_id'] = array(
'title' => 'Cases',
Expand Down Expand Up @@ -341,6 +349,16 @@ function civicrm_api3_activity_get($params) {
$sql->where('a.id IN (SELECT entity_id FROM civicrm_entity_tag WHERE entity_table = "civicrm_activity" AND !clause)', array('!clause' => $clause));
}
}
if (!empty($params['file_id'])) {
_civicrm_api3_validate_integer($params, 'file_id', $extraFieldSpecs['file_id'], 'Activity');
if (!is_array($params['file_id'])) {
$params['file_id'] = array('=' => $params['file_id']);
}
$clause = \CRM_Core_DAO::createSQLFilter('file_id', $params['file_id']);
if ($clause) {
$sql->where('a.id IN (SELECT entity_id FROM civicrm_entity_file WHERE entity_table = "civicrm_activity" AND !clause)', array('!clause' => $clause));
}
}
if (!empty($params['case_id'])) {
_civicrm_api3_validate_integer($params, 'case_id', $extraFieldSpecs['case_id'], 'Activity');
if (!is_array($params['case_id'])) {
Expand Down Expand Up @@ -401,6 +419,14 @@ function _civicrm_api3_activity_get_formatResult($params, $activities) {
$returns['assignee_contact_id'] = 1;
}

$tagGet = array('tag_id', 'entity_id');
foreach (array_keys($returns) as $key) {
if (strpos($key, 'tag_id.') === 0) {
$tagGet[] = $key;
$returns['tag_id'] = 1;
}
}

foreach ($returns as $n => $v) {
switch ($n) {
case 'assignee_contact_id':
Expand Down Expand Up @@ -438,11 +464,21 @@ function _civicrm_api3_activity_get_formatResult($params, $activities) {
$tags = civicrm_api3('EntityTag', 'get', array(
'entity_table' => 'civicrm_activity',
'entity_id' => array('IN' => array_keys($activities)),
'return' => array('tag_id', 'entity_id'),
'return' => $tagGet,
'options' => array('limit' => 0),
));
foreach ($tags['values'] as $tag) {
$activities[$tag['entity_id']]['tag_id'][] = (int) $tag['tag_id'];
$key = (int) $tag['entity_id'];
unset($tag['entity_id'], $tag['id']);
$activities[$key]['tag_id'][$tag['tag_id']] = $tag;
}
break;

case 'file_id':
$dao = CRM_Core_DAO::executeQuery("SELECT entity_id, file_id FROM civicrm_entity_file WHERE entity_table = 'civicrm_activity' AND entity_id IN (%1)",
array(1 => array(implode(',', array_keys($activities)), 'String', CRM_Core_DAO::QUERY_FORMAT_NO_QUOTES)));
while ($dao->fetch()) {
$activities[$dao->entity_id]['file_id'][] = $dao->file_id;
}
break;

Expand All @@ -453,7 +489,7 @@ function _civicrm_api3_activity_get_formatResult($params, $activities) {
}
}

// Legacy ouput
// Legacy extras
if (!empty($params['contact_id'])) {
$statusOptions = CRM_Activity_BAO_Activity::buildOptions('status_id', 'get');
$typeOptions = CRM_Activity_BAO_Activity::buildOptions('activity_type_id', 'validate');
Expand Down
50 changes: 45 additions & 5 deletions templates/CRM/Admin/Page/APIExplorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
docCodeTpl = _.template($('#doc-code-tpl').html()),
joinTpl = _.template($('#join-tpl').html()),

// The following apis do not support the syntax for joins
// FIXME: the solution is to convert these apis to use _civicrm_api3_basic_get
// The following apis do not use Api3SelectQuery so do not support advanced features like joins or OR
NO_JOINS = ['Contact', 'Contribution', 'Pledge', 'Participant'],

// These types of entityRef don't require any input to open
Expand Down Expand Up @@ -660,6 +659,7 @@
}
});
if (entity && action) {
handleAndOr();
formatQuery();
}
}
Expand Down Expand Up @@ -879,14 +879,14 @@
*/
function renderJoinSelector() {
$('#api-join').hide();
if (!_.includes(NO_JOINS, entity) && _.includes(['get', 'getsingle'], action)) {
if (!_.includes(NO_JOINS, entity) && _.includes(['get', 'getsingle', 'getcount'], action)) {
var joinable = {};
(function recurse(fields, joinable, prefix, depth, entities) {
_.each(fields, function(field) {
var name = prefix + field.name;
addJoinInfo(field, name);
var entity = field.FKApiName;
if (entity && field.is_core_field) {
if (entity && (field.is_core_field || field.supports_joins)) {
joinable[name] = {
title: field.title + ' (' + field.FKApiName + ')',
entity: entity,
Expand Down Expand Up @@ -938,6 +938,40 @@
}
}

function handleAndOr() {
if (!_.includes(NO_JOINS, entity) && _.includes(['get', 'getsingle', 'getcount'], action)) {
var or = [];
$('tr.api-param-row').each(function() {
if ($(this).next().is('tr.api-param-row') && $('input.api-param-name', this).val()) {
$('.api-and-or', this).show();
} else {
$(this).removeClass('or').find('.api-and-or').hide();
}
});
$('tr.api-param-row.or').each(function() {
var val = $(this).next().find('input.api-param-name').val();
if (val) {
if ($(this).prev().is('.or')) {
or[or.length - 1].push(val);
} else {
or.push([$('input.api-param-name', this).val(), val]);
}
}
});
if (or.length) {
params.options = params.options || {};
params.options.or = or;
}
} else {
$('.api-and-or').hide();
}
}

function toggleAndOr() {
$(this).closest('tr').toggleClass('or');
buildParams();
}

$(document).ready(function() {
// Set up tabs - bind active tab to document hash because... it's cool?
document.location.hash = document.location.hash || 'explorer';
Expand Down Expand Up @@ -996,7 +1030,13 @@
$(this).closest('tr').remove();
buildParams();
})
.on('change', 'select.api-chain-entity', getChainedAction);
.on('click', '.api-and-or > span', toggleAndOr)
.on('change', 'select.api-chain-entity', getChainedAction)
.on('sortupdate', buildParams)
.sortable({
handle: '.api-sort-handle',
items: '.api-chain-row, .api-param-row'
});
$('#api-join').on('change', 'input', onSelectJoin);
$('#example-entity').on('change', getExamples);
$('#example-action').on('change', getExample);
Expand Down
73 changes: 68 additions & 5 deletions templates/CRM/Admin/Page/APIExplorer.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
margin-top: 1em;
overflow: auto;
}
#api-params tr td {
padding-top: 13px;
}
#api-params-table th:first-child,
#api-params-table td:first-child {
width: 35%;
Expand All @@ -69,6 +72,61 @@
#api-params-table th:first-child + th + th {
width: 65%
}
#api-params .api-sort-handle {
margin-right: 10px;
cursor: move;
}
#api-params tr td > .crm-i,
#api-params tr td > a .crm-i {
color: lightgrey;
}
#api-params tr:hover td > .crm-i,
#api-params tr:hover td > a .crm-i {
color: grey;
}
#api-params .api-and-or {
margin-left: 1.2em;
font-size: .8em;
position: relative;
top: 5px;
width: 10em;
margin-bottom: -9px;
}
#api-params .api-and-or > span {
padding: 0 1em;
background: white;
cursor: pointer;
}
#api-params .api-or,
#api-params tr.or .api-and {
color: lightgrey;
}
#api-params tr.or .api-or {
color: inherit;
}
#api-params .api-and-or .crm-i {
transform: rotate(180deg);
}
#api-params tr.or .api-and-or .crm-i {
transform: initial;
}
#api-params .api-and-or:hover .crm-i {
color: #2786c2;
}
#api-params tr.or {
border-top: 3px solid lightgrey;
border-left: 3px solid lightgrey;
border-right: 3px solid lightgrey;
}
#api-params tr.or + tr {
border-left: 3px solid lightgrey;
border-right: 3px solid lightgrey;
border-bottom: 3px solid lightgrey;
}
#api-params tr.or + tr.or {
border-top: none;
border-bottom: none;
}
#api-generated td:first-child {
width: 60px;
}
Expand Down Expand Up @@ -171,13 +229,13 @@
<div id="mainTabContainer">
<ul>
<li class="ui-corner-all" title="GUI to build and execute API calls">
<a href="#explorer-tab">{ts}Explorer{/ts}</a>
<a href="#explorer-tab"><i class="crm-i fa-search"></i> {ts}Explorer{/ts}</a>
</li>
<li class="ui-corner-all" title="Auto-generated examples from the test suite">
<a href="#examples-tab">{ts}Examples{/ts}</a>
<a href="#examples-tab"><i class="crm-i fa-book"></i> {ts}Examples{/ts}</a>
</li>
<li class="ui-corner-all" title="API source-code and code-level documentation">
<a href="#docs-tab">{ts}Code Docs{/ts}</a>
<a href="#docs-tab"><i class="crm-i fa-code"></i> {ts}Code Docs{/ts}</a>
</li>
</ul>
Expand Down Expand Up @@ -303,7 +361,11 @@
{strip}
<script type="text/template" id="api-param-tpl">
<tr class="api-param-row">
<td><input style="width: 100%;" class="crm-form-text api-param-name api-input" value="<%= name %>" placeholder="{ts}Parameter{/ts}" /></td>
<td>
<i class="crm-i api-sort-handle fa-arrows"></i>
<input style="width: 90%;" class="crm-form-text api-param-name api-input" value="<%= name %>" placeholder="{ts}Parameter{/ts}" />
<div class="api-and-or"><span><span class="api-and">{ts}AND{/ts}</span> <i class="crm-i fa-toggle-on"></i> <span class="api-or">{ts}OR{/ts}</span></span></div>
</td>
<td>
{literal}
<% if (noOps) { %>
Expand Down Expand Up @@ -357,7 +419,8 @@
<script type="text/template" id="api-chain-tpl">
<tr class="api-chain-row">
<td>
<select style="width: 100%;" class="crm-form-select api-chain-entity">
<i class="crm-i api-sort-handle fa-arrows"></i>
<select style="width: 90%;" class="crm-form-select api-chain-entity">
<option value=""></option>
{foreach from=$entities.values item=entity}
<option value="{$entity}" {if !empty($entities.deprecated) && in_array($entity, $entities.deprecated)}class="strikethrough"{/if}>
Expand Down
Loading

0 comments on commit 93f1eff

Please sign in to comment.