CRM_Utils_System::civiExit();
}
try {
- // Call multiple
+ // Two call formats. Which one was used? Note: CRM_Api4_Permission::check() and CRM_Api4_Page_AJAX::run() should have matching conditionals.
if (empty($this->urlPath[3])) {
+ // Received multi-call format
$calls = CRM_Utils_Request::retrieve('calls', 'String', CRM_Core_DAO::$_nullObject, TRUE, NULL, 'POST');
$calls = json_decode($calls, TRUE);
$response = [];
$response[$index] = call_user_func_array([$this, 'execute'], $call);
}
}
- // Call single
else {
+ // Received single-call format
$entity = $this->urlPath[3];
$action = $this->urlPath[4];
$params = CRM_Utils_Request::retrieve('params', 'String');
class CRM_Api4_Permission {
public static function check() {
- $config = CRM_Core_Config::singleton();
- $urlPath = explode('/', $_GET[$config->userFrameworkURLVar]);
- $permissions = [
+ $urlPath = explode('/', CRM_Utils_System::currentPath());
+ $defaultPermissions = [
['access CiviCRM', 'access AJAX API'],
];
+ // Two call formats. Which one was used? Note: CRM_Api4_Permission::check() and CRM_Api4_Page_AJAX::run() should have matching conditionals.
if (!empty($urlPath[3])) {
+ // Received single-call format
$entity = $urlPath[3];
$action = $urlPath[4];
+ $permissions = $defaultPermissions;
CRM_Utils_Hook::alterApiRoutePermissions($permissions, $entity, $action);
+ return CRM_Core_Permission::check($permissions);
+ }
+ else {
+ // Received multi-call format
+ $calls = CRM_Utils_Request::retrieve('calls', 'String', CRM_Core_DAO::$_nullObject, TRUE, NULL, 'POST');
+ $calls = json_decode($calls, TRUE);
+ foreach ($calls as $call) {
+ $permissions = $defaultPermissions;
+ CRM_Utils_Hook::alterApiRoutePermissions($permissions, $call[0], $call[1]);
+ if (!CRM_Core_Permission::check($permissions)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
}
- return CRM_Core_Permission::check($permissions);
}
}
*
* Generated from xml/schema/CRM/Contact/SavedSearch.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:1b324e028960b1c8fd94ecdc973a6fdc)
+ * (GenCodeChecksum:463e44ca73e5b034502fcc1605f5a9c2)
*/
/**
*/
public static $_icon = 'fa-search-plus';
+ /**
+ * Field to show when displaying a record.
+ *
+ * @var string
+ */
+ public static $_labelField = 'label';
+
/**
* Should CiviCRM log any modifications to this table in the civicrm_log table.
*
$error = $this->checkContactDuplicate($paramValues);
if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
- $matchedIDs = explode(',', $error['error_message']['params'][0]);
+ $matchedIDs = (array) $error['error_message']['params'];
if (count($matchedIDs) > 1) {
throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The contribution was not imported', CRM_Import_Parser::ERROR);
}
$error = $this->checkContactDuplicate($params);
if (isset($error['error_message']['params'][0])) {
- $matchedIDs = explode(',', $error['error_message']['params'][0]);
+ $matchedIDs = (array) $error['error_message']['params'];
// check if only one contact is found
if (count($matchedIDs) > 1) {
* @return array
* array of measurement units
*/
- public static function getUnits() {
- return [
- 'in' => ts('Inches'),
- 'cm' => ts('Centimeters'),
- 'mm' => ts('Millimeters'),
- 'pt' => ts('Points'),
- ];
+ public static function getUnits(): array {
+ return CRM_Core_SelectValues::getLayoutUnits();
}
/**
* @return array
* array of measurement units
*/
- public static function getUnits() {
- return [
- 'in' => ts('Inches'),
- 'cm' => ts('Centimeters'),
- 'mm' => ts('Millimeters'),
- 'pt' => ts('Points'),
- ];
+ public static function getUnits(): array {
+ return CRM_Core_SelectValues::getLayoutUnits();
}
/**
$this->_paymentProcessor = $paymentProcessor;
}
+ /**
+ * @var GuzzleHttp\Client
+ */
+ protected $guzzleClient;
+
+ /**
+ * @return \GuzzleHttp\Client
+ */
+ public function getGuzzleClient(): \GuzzleHttp\Client {
+ return $this->guzzleClient ?? new \GuzzleHttp\Client();
+ }
+
+ /**
+ * @param \GuzzleHttp\Client $guzzleClient
+ */
+ public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) {
+ $this->guzzleClient = $guzzleClient;
+ }
+
/**
* Map fields to parameters.
*
// Send to the payment processor using cURL
$chHost = $host . '?xmldata=' . $xml;
-
- $ch = curl_init($chHost);
- if (!$ch) {
- throw new PaymentProcessorException('Could not initiate connection to payment gateway', 9004);
- }
-
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0);
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
- // return the result on success, FALSE on failure
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($ch, CURLOPT_TIMEOUT, 36000);
- // set this for debugging -look for output in apache error log
- //curl_setopt ($ch,CURLOPT_VERBOSE,1 );
- // ensures any Location headers are followed
+ $curlParams = [
+ CURLOPT_RETURNTRANSFER => TRUE,
+ CURLOPT_TIMEOUT => 36000,
+ CURLOPT_SSL_VERIFYHOST => Civi::settings()->get('verifySSL') ? 2 : 0,
+ CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'),
+ ];
if (ini_get('open_basedir') == '') {
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
- }
-
- // Send the data out over the wire
- $responseData = curl_exec($ch);
-
- // See if we had a curl error - if so tell 'em and bail out
- // NOTE: curl_error does not return a logical value (see its documentation), but
- // a string, which is empty when there was no error.
- if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
- curl_close($ch);
- $errorNum = curl_errno($ch);
- $errorDesc = curl_error($ch);
-
- // Paranoia - in the unlikley event that 'curl' errno fails
- if ($errorNum == 0) {
- $errorNum = 9005;
- }
-
- // Paranoia - in the unlikley event that 'curl' error fails
- if (strlen($errorDesc) == 0) {
- $errorDesc = 'Connection to payment gateway failed';
- }
- throw new PaymentProcessorException('Curl error - ' . $errorDesc . ' Try this link for more information http://curl.haxx.se/docs/sslcerts.html', $errorNum);
- }
-
- // If null data returned - tell 'em and bail out
- // NOTE: You will not necessarily get a string back, if the request failed for
- // any reason, the return value will be the boolean false.
- if (($responseData === FALSE) || (strlen($responseData) == 0)) {
- curl_close($ch);
- throw new PaymentProcessorException('Error: Connection to payment gateway failed - no data returned.', 9006);
+ $curlParams[CURLOPT_FOLLOWLOCATION] = 1;
}
+ $responseData = $this->getGuzzleClient()->post($chHost, [
+ 'curl' => $curlParams,
+ ])->getBody();
// If gateway returned no data - tell 'em and bail out
if (empty($responseData)) {
- curl_close($ch);
throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007);
}
- // Success so far - close the curl and check the data
- curl_close($ch);
-
// Payment successfully sent to gateway - process the response now
-
$processorResponse = $this->decodeXMLresponse($responseData);
// success in test mode returns response "APPROVED"
// test mode always returns trxn_id = 0
// fix for CRM-2566
-
if ($processorResponse['errorCode']) {
throw new PaymentProcessorException("Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . '] - from payment processor', 9010);
}
];
}
+ /**
+ * Get measurement units recognized by the TCPDF package used to create PDF labels.
+ *
+ * @return array
+ * array of measurement units
+ */
+ public static function getLayoutUnits(): array {
+ return [
+ 'in' => ts('Inches'),
+ 'cm' => ts('Centimeters'),
+ 'mm' => ts('Millimeters'),
+ 'pt' => ts('Points'),
+ 'px' => ts('Pixels'),
+ ];
+ }
+
/**
* Extension types.
*
$error = $this->checkContactDuplicate($formatValues);
if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
- $matchedIDs = explode(',', $error['error_message']['params'][0]);
+ $matchedIDs = (array) $error['error_message']['params'];
if (count($matchedIDs) >= 1) {
foreach ($matchedIDs as $contactId) {
$formatted['contact_id'] = $contactId;
$error = $this->checkContactDuplicate($formatValues);
if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
- $matchedIDs = explode(',', $error['error_message']['params'][0]);
+ $matchedIDs = (array) $error['error_message']['params'];
if (count($matchedIDs) > 1) {
throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The membership was not imported', CRM_Import_Parser::ERROR);
}
$this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
$this->addTask('Replace %A specifier in date settings.', 'replacePercentA');
$this->addTask('Add invoice pdf format', 'addInvoicePDFFormat');
+ $this->addTask('Add Recent Items Providers', 'addRecentItemsProviders');
}
/**
return $usages;
}
+ /**
+ * dev/core#3783 Add Recent Items Providers.
+ * @return bool
+ */
+ public static function addRecentItemsProviders() {
+ CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+ 'name' => 'recent_items_providers',
+ 'title' => ts('Recent Items Providers'),
+ 'is_reserved' => 0,
+ ]);
+ $values = [
+ 'Contact' => ['label' => ts('Contacts')],
+ 'Relationship' => ['label' => ts('Relationships')],
+ 'Activity' => ['label' => ts('Activities')],
+ 'Note' => ['label' => ts('Notes')],
+ 'Group' => ['label' => ts('Groups')],
+ 'Case' => ['label' => ts('Cases')],
+ 'Contribution' => ['label' => ts('Contributions')],
+ 'Participant' => ['label' => ts('Participants')],
+ 'Membership' => ['label' => ts('Memberships')],
+ 'Pledge' => ['label' => ts('Pledges')],
+ 'Event' => ['label' => ts('Events')],
+ 'Campaign' => ['label' => ts('Campaigns')],
+ ];
+ foreach ($values as $name => $value) {
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists($value + [
+ 'name' => $name,
+ 'option_group_id' => 'recent_items_providers',
+ ]);
+ }
+ return TRUE;
+ }
+
}
'is_error' => 1,
'error_message' => [
'code' => CRM_Core_Error::DUPLICATE_CONTACT,
- 'params' => $contact->id,
+ 'params' => [$contact->id],
'level' => 'Fatal',
'message' => "Found matching contacts: $contact->id",
],
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
+use Civi\Api4\OptionValue;
use Civi\Api4\Utils\CoreUtil;
/**
/**
* Gets the list of available providers to civi's recent items stack
*
- * TODO: Make this an option group so extensions can extend it.
- *
* @return array
*/
public static function getProviders() {
- $providers = [
- 'Contact' => ts('Contacts'),
- 'Relationship' => ts('Relationships'),
- 'Activity' => ts('Activities'),
- 'Note' => ts('Notes'),
- 'Group' => ts('Groups'),
- 'Case' => ts('Cases'),
- 'Contribution' => ts('Contributions'),
- 'Participant' => ts('Participants'),
- 'Grant' => ts('Grants'),
- 'Membership' => ts('Memberships'),
- 'Pledge' => ts('Pledges'),
- 'Event' => ts('Events'),
- 'Campaign' => ts('Campaigns'),
- ];
-
- return $providers;
+ return OptionValue::get(FALSE)
+ ->addWhere('option_group_id:name', '=', 'recent_items_providers')
+ ->addWhere('is_active', '=', TRUE)
+ ->addOrderBy('weight', 'ASC')
+ ->execute()
+ ->indexBy('value')
+ ->column('label');
}
}
* An HTML string containing a link to the given path.
*/
public static function url(
- $path = NULL,
- $query = NULL,
+ $path = '',
+ $query = '',
$absolute = FALSE,
$fragment = NULL,
$htmlize = TRUE,
$frontend = FALSE,
$forceBackend = FALSE
) {
+ // handle legacy null params
+ $path = $path ?? '';
+ $query = $query ?? '';
+
$query = self::makeQueryString($query);
// Legacy handling for when the system passes around html escaped strings
- if (strstr(($query ?? ''), '&')) {
+ if (strstr($query, '&')) {
$query = html_entity_decode($query);
}
// Extract fragment from path or query if munged together
- if ($query && strstr(($query ?? ''), '#')) {
+ if ($query && strstr($query, '#')) {
list($path, $fragment) = explode('#', $query);
}
if ($path && strstr($path, '#')) {
$separator = '&';
if (!$config->cleanURL) {
- if (isset($path)) {
- if (isset($query)) {
+ if ($path !== NULL && $path !== '' && $path !== FALSE) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $script . '?q=' . $path . $separator . $query . $fragment;
}
else {
}
}
else {
- if (isset($query)) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $script . '?' . $query . $fragment;
}
else {
}
}
else {
- if (isset($path)) {
- if (isset($query)) {
+ if ($path !== NULL && $path !== '' && $path !== FALSE) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $path . '?' . $query . $fragment;
}
else {
}
}
else {
- if (isset($query)) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $script . '?' . $query . $fragment;
}
else {
* @return bool
*/
public function isFrontEndPage() {
- $path = CRM_Utils_System::currentPath();
+ $path = CRM_Utils_System::currentPath() ?? '';
// Get the menu for above URL.
$item = CRM_Core_Menu::get($path);
$separator = ($htmlize && $frontend) ? '&' : '&';
if (!$config->cleanURL) {
- if (isset($path)) {
- if (isset($query)) {
+ if ($path !== NULL && $path !== '' && $path !== FALSE) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $script . '?q=' . $path . $separator . $query . $fragment;
}
else {
}
}
else {
- if (isset($query)) {
+ if ($query !== NULL && $query !== '' && $query !== FALSE) {
return $base . $script . '?' . $query . $fragment;
}
else {
namespace Civi\Api4\Action\Contact;
+use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\FormattingUtil;
+
/**
* Code shared by Contact create/update/save actions
*/
}
}
}
- return parent::write($items);
+ $saved = parent::write($items);
+ foreach ($items as $index => $item) {
+ self::saveLocations($item, $saved[$index]);
+ }
+ return $saved;
+ }
+
+ /**
+ * @param array $params
+ * @param \CRM_Contact_DAO_Contact $contact
+ */
+ protected function saveLocations(array $params, $contact) {
+ foreach (['Address', 'Email', 'Phone', 'IM'] as $entity) {
+ foreach (['primary', 'billing'] as $type) {
+ $prefix = strtolower($entity) . '_' . $type . '.';
+ $item = FormattingUtil::filterByPrefix($params, $prefix . '*', '*');
+ // Not allowed to update by id or alter primary or billing flags
+ unset($item['id'], $item['is_primary'], $item['is_billing']);
+ if ($item) {
+ $labelField = CoreUtil::getInfoItem($entity, 'label_field');
+ // If NULL was given for the main field (e.g. `email`) then delete the record
+ if ($labelField && array_key_exists($labelField, $item) && is_null($item[$labelField])) {
+ civicrm_api4($entity, 'delete', [
+ 'checkPermissions' => FALSE,
+ 'where' => [
+ ['contact_id', '=', $contact->id],
+ ["is_$type", '=', TRUE],
+ ],
+ ]);
+ }
+ else {
+ $item['contact_id'] = $contact->id;
+ $item["is_$type"] = TRUE;
+ $saved = civicrm_api4($entity, 'save', [
+ 'checkPermissions' => FALSE,
+ 'records' => [$item],
+ 'match' => ['contact_id', "is_$type"],
+ ])->first();
+ foreach ($saved as $key => $value) {
+ $key = $prefix . $key;
+ $contact->$key = $value;
+ }
+ }
+ }
+ }
+ }
}
}
--- /dev/null
+<?php
+
+namespace Civi\Api4\Event\Subscriber;
+
+use Civi\Api4\Event\Events;
+use Civi\Api4\Event\SchemaMapBuildEvent;
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ContactSchemaMapSubscriber implements EventSubscriberInterface {
+
+ /**
+ * @return array
+ */
+ public static function getSubscribedEvents() {
+ return [
+ Events::SCHEMA_MAP_BUILD => 'onSchemaBuild',
+ ];
+ }
+
+ /**
+ * @param \Civi\Api4\Event\SchemaMapBuildEvent $event
+ */
+ public function onSchemaBuild(SchemaMapBuildEvent $event) {
+ $schema = $event->getSchemaMap();
+ $table = $schema->getTableByName('civicrm_contact');
+
+ // Add links to primary & billing email, address, phone & im
+ foreach (['email', 'address', 'phone', 'im'] as $ent) {
+ foreach (['primary', 'billing'] as $type) {
+ $link = new Joinable("civicrm_$ent", 'contact_id', "{$ent}_$type");
+ $link->setBaseTable('civicrm_contact');
+ $link->setJoinType(Joinable::JOIN_TYPE_ONE_TO_ONE);
+ $link->addCondition("`{target_table}`.`is_$type` = 1");
+ $table->addTableLink('id', $link);
+ }
+ }
+ }
+
+}
* @return array
*/
public function baoToArray($bao, $input) {
- $allFields = array_column($bao->fields(), 'name');
+ $entityFields = array_column($bao->fields(), 'name');
+ $inputFields = array_map(function($key) {
+ return explode(':', $key)[0];
+ }, array_keys($input));
+ $combinedFields = array_unique(array_merge($entityFields, $inputFields));
if (!empty($this->reload)) {
- $inputFields = $allFields;
$bao->find(TRUE);
}
else {
- $inputFields = array_keys($input);
// Convert 'null' input to true null
foreach ($inputFields as $key) {
if (($bao->$key ?? NULL) === 'null') {
}
}
$values = [];
- foreach ($allFields as $field) {
- if (isset($bao->$field) || in_array($field, $inputFields)) {
+ foreach ($combinedFields as $field) {
+ if (isset($bao->$field) || in_array($field, $inputFields) || (!empty($this->reload) && in_array($field, $entityFields))) {
$values[$field] = $bao->$field ?? NULL;
}
}
// If we're not explicitly referencing the ID (or some other FK field) of the joinEntity, search for a default
if (!$explicitFK) {
foreach ($this->apiFieldSpec as $name => $field) {
- if (is_array($field) && $field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
+ if (!is_array($field) || $field['type'] !== 'Field') {
+ continue;
+ }
+ if ($field['entity'] !== $joinEntity && $field['fk_entity'] === $joinEntity) {
$conditions[] = $this->treeWalkClauses([$name, '=', "$alias.id"], 'ON');
}
elseif (strpos($name, "$alias.") === 0 && substr_count($name, '.') === 1 && $field['fk_entity'] === $this->getEntity()) {
protected $alias;
/**
- * @var array
+ * @var string[]
*/
protected $conditions = [];
* @return array
*/
public function getConditionsForJoin(string $baseTableAlias, string $tableAlias) {
- $baseCondition = sprintf(
+ $conditions = [];
+ $conditions[] = sprintf(
'`%s`.`%s` = `%s`.`%s`',
$baseTableAlias,
$this->baseColumn,
$tableAlias,
$this->targetColumn
);
-
- return array_merge([$baseCondition], $this->conditions);
+ foreach ($this->conditions as $condition) {
+ $conditions[] = str_replace(['{base_table}', '{target_table}'], [$baseTableAlias, $tableAlias], $condition);
+ }
+ return $conditions;
}
/**
}
/**
- * @param $condition
+ * @param string $condition
*
* @return $this
*/
- public function addCondition($condition) {
+ public function addCondition(string $condition) {
$this->conditions[] = $condition;
return $this;
}
/**
- * @param array $conditions
+ * @param string[] $conditions
*
* @return $this
*/
->setSqlRenderer([__CLASS__, 'calculateAge']);
$spec->addFieldSpec($field);
}
+
+ // Address, Email, Phone, IM
+ $entities = [
+ 'Address' => [
+ 'primary' => [
+ 'title' => ts('Primary Address ID'),
+ 'label' => ts('Primary Address'),
+ ],
+ 'billing' => [
+ 'title' => ts('Billing Address ID'),
+ 'label' => ts('Billing Address'),
+ ],
+ ],
+ 'Email' => [
+ 'primary' => [
+ 'title' => ts('Primary Email ID'),
+ 'label' => ts('Primary Email'),
+ ],
+ 'billing' => [
+ 'title' => ts('Billing Email ID'),
+ 'label' => ts('Billing Email'),
+ ],
+ ],
+ 'Phone' => [
+ 'primary' => [
+ 'title' => ts('Primary Phone ID'),
+ 'label' => ts('Primary Phone'),
+ ],
+ 'billing' => [
+ 'title' => ts('Billing Phone ID'),
+ 'label' => ts('Billing Phone'),
+ ],
+ ],
+ 'IM' => [
+ 'primary' => [
+ 'title' => ts('Primary IM ID'),
+ 'label' => ts('Primary IM'),
+ ],
+ 'billing' => [
+ 'title' => ts('Billing IM ID'),
+ 'label' => ts('Billing IM'),
+ ],
+ ],
+ ];
+ foreach ($entities as $entity => $types) {
+ foreach ($types as $type => $info) {
+ $name = strtolower($entity) . '_' . $type;
+ $field = new FieldSpec($name, 'Contact', 'String');
+ $field->setLabel($info['label'])
+ ->setTitle($info['title'])
+ ->setColumnName('id')
+ ->setType('Extra')
+ ->setFkEntity($entity)
+ ->setSqlRenderer([__CLASS__, 'getLocationFieldSql']);
+ $spec->addFieldSpec($field);
+ }
+ }
+
}
/**
return "TIMESTAMPDIFF(YEAR, {$field['sql_name']}, CURDATE())";
}
+ /**
+ * Generate SQL for address/email/phone/im id field
+ * @param array $field
+ * @param \Civi\Api4\Query\Api4SelectQuery $query
+ * @return string
+ */
+ public static function getLocationFieldSql(array $field, Api4SelectQuery $query) {
+ $prefix = empty($field['explicit_join']) ? '' : $field['explicit_join'] . '.';
+ $idField = $query->getField($prefix . $field['name'] . '.id');
+ return $idField['sql_name'];
+ }
+
}
var fieldInfo = _.cloneDeep(_.findWhere(getEntity().actions, {name: action}).fields);
fieldList.length = 0;
if (addPseudoconstant) {
- addPseudoconstants(fieldInfo, addPseudoconstant);
+ addPseudoconstants(fieldInfo);
}
if (addWriteJoins) {
addWriteJoinFields(fieldInfo);
});
}
if (addPseudoconstant) {
- addPseudoconstants(joinFields, addPseudoconstant);
+ addPseudoconstants(joinFields);
}
fieldList.push({
text: join.entity + ' AS ' + join.alias,
var linkFields = _.cloneDeep(entityFields(field.fk_entity)),
wildCard = addWildcard ? [{id: field.name + '.*', text: field.name + '.*', 'description': 'All core ' + field.fk_entity + ' fields'}] : [];
if (addPseudoconstant) {
- addPseudoconstants(linkFields, addPseudoconstant);
+ addPseudoconstants(linkFields);
}
fieldList.push({
text: field.name,
}
// Note: this function transforms a raw list a-la getFields; not a select2-formatted list
- function addPseudoconstants(fieldList, toAdd) {
+ function addPseudoconstants(fieldList) {
var optionFields = _.filter(fieldList, 'options');
_.each(optionFields, function(field) {
var pos = _.findIndex(fieldList, {name: field.name}) + 1;
- _.each(toAdd, function(suffix) {
+ _.each(field.suffixes, function(suffix) {
var newField = _.cloneDeep(field);
newField.name += ':' + suffix;
fieldList.splice(pos, 0, newField);
$scope.fieldList = function(param) {
return function() {
var fields = [];
- getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name'], true);
+ getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, true, true);
// Disable fields that are already in use
_.each($scope.params[param] || [], function(val) {
var usedField = val[0].replace(/[:.]name/, '');
this.buildFieldList = function() {
var actionInfo = _.findWhere(actions, {id: $scope.action});
getFieldList($scope.fields, $scope.action);
- getFieldList($scope.fieldsAndJoins, $scope.action, ['name']);
+ getFieldList($scope.fieldsAndJoins, $scope.action, true);
getFieldList($scope.fieldsAndJoinsAndFunctions, $scope.action);
- getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, ['name', 'label']);
- getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, ['name', 'label']);
+ getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, true);
+ getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, true);
if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
addJoins($scope.fieldsAndJoins);
// SQL functions are supported if HAVING is
$scope.fieldsAndJoinsAndFunctionsAndWildcards.push(functions);
}
addJoins($scope.fieldsAndJoinsAndFunctions, true);
- addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, ['name', 'label']);
- addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, ['name', 'label']);
+ addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, true);
+ addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, true);
}
// Custom fields are supported if HAVING is
if (actionInfo.params.having) {
$data = ['fields' => $result[$id]];
foreach ($entity['joins'] ?? [] as $joinEntity => $join) {
$data['joins'][$joinEntity] = (array) $api4($joinEntity, 'get', [
- 'where' => self::getJoinWhereClause($entity['type'], $joinEntity, $id),
+ 'where' => self::getJoinWhereClause($this->_formDataModel, $entity['name'], $joinEntity, $id),
'limit' => !empty($join['af-repeat']) ? $join['max'] ?? 0 : 1,
'select' => array_keys($join['fields']),
'orderBy' => self::getEntityField($joinEntity, 'is_primary') ? ['is_primary' => 'DESC'] : [],
abstract protected function processForm();
/**
- * @param $mainEntityName
- * @param $joinEntityName
- * @param $mainEntityId
+ * @param \Civi\Afform\FormDataModel $formDataModel
+ * @param string $mainEntityName
+ * @param string $joinEntityType
+ * @param int|string $mainEntityId
* @return array
* @throws \API_Exception
*/
- protected static function getJoinWhereClause($mainEntityName, $joinEntityName, $mainEntityId) {
+ protected static function getJoinWhereClause(FormDataModel $formDataModel, string $mainEntityName, string $joinEntityType, $mainEntityId) {
+ $entity = $formDataModel->getEntity($mainEntityName);
+ $mainEntityType = $entity['type'];
$params = [];
- if (self::getEntityField($joinEntityName, 'entity_id')) {
+
+ // Add data as clauses e.g. `is_primary: true`
+ foreach ($entity['joins'][$joinEntityType]['data'] ?? [] as $key => $val) {
+ $params[] = [$key, '=', $val];
+ }
+
+ // Figure out the FK field between the join entity and the main entity
+ if (self::getEntityField($joinEntityType, 'entity_id')) {
$params[] = ['entity_id', '=', $mainEntityId];
- if (self::getEntityField($joinEntityName, 'entity_table')) {
- $params[] = ['entity_table', '=', CoreUtil::getTableName($mainEntityName)];
+ if (self::getEntityField($joinEntityType, 'entity_table')) {
+ $params[] = ['entity_table', '=', CoreUtil::getTableName($mainEntityType)];
}
}
else {
- $mainEntityField = \CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($mainEntityName) . '_id';
+ $mainEntityField = \CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($mainEntityType) . '_id';
$params[] = [$mainEntityField, '=', $mainEntityId];
}
return $params;
$result = civicrm_api4($joinEntityName, 'replace', [
// Disable permission checks because the main entity has already been vetted
'checkPermissions' => FALSE,
- 'where' => self::getJoinWhereClause($event->getEntityType(), $joinEntityName, $entityId),
+ 'where' => self::getJoinWhereClause($event->getFormDataModel(), $event->getEntityName(), $joinEntityName, $entityId),
'records' => $values,
], ['id']);
$indexedResult = array_combine(array_keys($values), (array) $result);
civicrm_api4($joinEntityName, 'delete', [
// Disable permission checks because the main entity has already been vetted
'checkPermissions' => FALSE,
- 'where' => self::getJoinWhereClause($event->getEntityType(), $joinEntityName, $entityId),
+ 'where' => self::getJoinWhereClause($event->getFormDataModel(), $event->getEntityName(), $joinEntityName, $entityId),
]);
}
catch (\API_Exception $e) {
--- /dev/null
+<?php
+use CRM_Grant_ExtensionUtil as E;
+
+// Prevent errors during upgrades from < 5.53 that don't yet have the option group
+$optionGroup = \Civi\Api4\OptionGroup::get(FALSE)
+ ->addWhere('name', '=', 'recent_items_providers')
+ ->selectRowCount()
+ ->execute();
+if (!$optionGroup->count()) {
+ return [];
+}
+
+return [
+ [
+ 'name' => 'OptionGroup_recent_items_providers_OptionValue_Grant',
+ 'entity' => 'OptionValue',
+ 'cleanup' => 'always',
+ 'update' => 'always',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'option_group_id.name' => 'recent_items_providers',
+ 'label' => E::ts('Grants'),
+ 'value' => 'Grant',
+ 'name' => 'Grants',
+ 'grouping' => NULL,
+ 'filter' => 0,
+ 'is_default' => FALSE,
+ 'description' => NULL,
+ 'is_optgroup' => FALSE,
+ 'is_reserved' => FALSE,
+ 'is_active' => TRUE,
+ 'icon' => NULL,
+ 'color' => NULL,
+ 'component_id' => NULL,
+ 'domain_id' => NULL,
+ 'visibility_id' => NULL,
+ ],
+ ],
+ ],
+];
array_splice($entity['fields'], $index, 0, [$newField]);
}
}
+ // Useful address fields (see ContactSchemaMapSubscriber)
+ if ($entity['name'] === 'Contact') {
+ $addressFields = ['city', 'state_province_id', 'country_id'];
+ foreach ($addressFields as $fieldName) {
+ foreach (['primary', 'billing'] as $type) {
+ $newField = \CRM_Utils_Array::findAll($schema['Address']['fields'], ['name' => $fieldName])[0];
+ $newField['name'] = "address_$type.$fieldName";
+ $arg = [1 => $newField['label']];
+ $newField['label'] = $type === 'primary' ? ts('Address (primary) %1', $arg) : ts('Address (billing) %1', $arg);
+ $entity['fields'][] = $newField;
+ }
+ }
+ }
}
}
return array_values($schema);
$this->assertEquals('No matching Contact found for (mum@example.com )', $row['_status_message']);
}
+ public function testImportWithMatchByExternalIdentifier() :void {
+ CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_contact AUTO_INCREMENT = 1000000");
+
+ $contactRubyParams = [
+ 'first_name' => 'Ruby',
+ 'external_identifier' => 'ruby',
+ 'contact_type' => 'Individual',
+ ];
+ $contactSapphireParams = [
+ 'first_name' => 'Sapphire',
+ 'external_identifier' => 'sapphire',
+ 'contact_type' => 'Individual',
+ ];
+ $contactRubyId = $this->individualCreate($contactRubyParams);
+ $contactSapphireId = $this->individualCreate($contactSapphireParams);
+
+ // make sure we're testing dev/core#3784
+ self::assertEquals(1, substr($contactRubyId, 0, 1));
+ self::assertEquals(1, substr($contactSapphireId, 0, 1));
+
+ $mapping = [
+ ['name' => 'external_identifier'],
+ ['name' => 'total_amount'],
+ ['name' => 'receive_date'],
+ ['name' => 'financial_type_id'],
+ ];
+ $this->importCSV('contributions_match_external_id.csv', $mapping);
+
+ $contributionsOfRuby = Contribution::get()
+ ->addWhere('contact_id', '=', $contactRubyId)->execute();
+ $contributionsOfSapphire = Contribution::get()
+ ->addWhere('contact_id', '=', $contactSapphireId)->execute();
+
+ $this->assertCount(1, $contributionsOfRuby, 'Wrong number of contributions imported');
+ $this->assertCount(1, $contributionsOfSapphire, 'Wrong number of contributions imported');
+ $this->assertEquals(22222, $contributionsOfRuby->first()['total_amount']);
+ $this->assertEquals(5, $contributionsOfSapphire->first()['total_amount']);
+
+ $dataSource = new CRM_Import_DataSource_CSV($this->userJobID);
+ $this->assertEquals(0, $dataSource->getRowCount([CRM_Import_Parser::ERROR]));
+ $this->assertEquals(2, $dataSource->getRowCount([CRM_Import_Parser::VALID]));
+ }
+
/**
* Run the import parser.
*
--- /dev/null
+External Identifier,Total Amount,Receive Date,Financial Type
+sapphire,5,2005-05-05,Donation
+ruby,22222,2022-02-22,Donation
}
+ public function testGetWithPrimaryEmailPhoneIMAddress() {
+ $lastName = uniqid(__FUNCTION__);
+ $email = uniqid() . '@example.com';
+ $phone = uniqid('phone');
+ $im = uniqid('im');
+ $c1 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+ $c2 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+ $c3 = $this->createTestRecord('Contact', ['last_name' => $lastName]);
+
+ $this->createTestRecord('Email', ['email' => $email, 'contact_id' => $c1['id']]);
+ $this->createTestRecord('Email', ['email' => 'not@primary.com', 'contact_id' => $c1['id']]);
+ $this->createTestRecord('Phone', ['phone' => $phone, 'contact_id' => $c1['id']]);
+ $this->createTestRecord('IM', ['name' => $im, 'contact_id' => $c2['id']]);
+ $this->createTestRecord('Address', ['city' => 'Somewhere', 'street_address' => '123 Street', 'contact_id' => $c2['id']]);
+
+ $results = Contact::get(FALSE)
+ ->addSelect('id', 'email_primary.email', 'phone_primary.phone', 'im_primary.name', 'address_primary.*')
+ ->addWhere('last_name', '=', $lastName)
+ ->addOrderBy('id')
+ ->execute();
+
+ $this->assertEquals($email, $results[0]['email_primary.email']);
+ $this->assertEquals($phone, $results[0]['phone_primary.phone']);
+ $this->assertEquals($im, $results[1]['im_primary.name']);
+ $this->assertEquals('Somewhere', $results[1]['address_primary.city']);
+ $this->assertEquals('123 Street', $results[1]['address_primary.street_address']);
+ $this->assertNull($results[0]['im_primary.name']);
+ $this->assertNull($results[2]['email_primary.email']);
+ $this->assertNull($results[2]['phone_primary.phone']);
+ $this->assertNull($results[2]['im_primary.name']);
+ $this->assertNull($results[2]['address_primary.city']);
+ }
+
}
*/
class NullValueTest extends Api4TestBase implements TransactionalInterface {
- public function setUpHeadless() {
+ public function setUp(): void {
$format = '{contact.first_name}{ }{contact.last_name}';
\Civi::settings()->set('display_name_format', $format);
- return parent::setUpHeadless();
+ parent::setUp();
}
public function testStringNull() {
namespace api\v4\Entity;
+use Civi\Api4\Address;
use Civi\Api4\Contact;
+use Civi\Api4\Email;
use Civi\Api4\OptionValue;
use api\v4\Api4TestBase;
+use Civi\Api4\Phone;
/**
* @group headless
$this->assertEquals($labels, $fetchedContact['preferred_communication_method:label']);
}
+ public function testCreateWithPrimaryAndBilling() {
+ $contact = $this->createTestRecord('Contact', [
+ 'email_primary.email' => 'a@test.com',
+ 'email_billing.email' => 'b@test.com',
+ 'address_billing.city' => 'Hello',
+ 'address_billing.state_province_id:abbr' => 'AK',
+ 'address_billing.country_id:abbr' => 'USA',
+ ]);
+ $addr = Address::get(FALSE)
+ ->addWhere('contact_id', '=', $contact['id'])
+ ->execute();
+ $this->assertCount(1, $addr);
+ $this->assertEquals('Hello', $contact['address_billing.city']);
+ $this->assertEquals(1228, $contact['address_billing.country_id']);
+ $emails = Email::get(FALSE)
+ ->addWhere('contact_id', '=', $contact['id'])
+ ->execute();
+ $this->assertCount(2, $emails);
+ $this->assertEquals('a@test.com', $contact['email_primary.email']);
+ $this->assertEquals('b@test.com', $contact['email_billing.email']);
+ }
+
+ public function testUpdateDeletePrimaryAndBilling() {
+ $contact = $this->createTestRecord('Contact', [
+ 'phone_primary.phone' => '12345',
+ 'phone_billing.phone' => '54321',
+ ]);
+ Contact::update(FALSE)
+ ->addValue('id', $contact['id'])
+ // Delete primary phone, update billing phone
+ ->addValue('phone_primary.phone', NULL)
+ ->addValue('phone_billing.phone', 99999)
+ ->execute();
+ $phone = Phone::get(FALSE)
+ ->addWhere('contact_id', '=', $contact['id'])
+ ->execute()
+ ->single();
+ $this->assertEquals('99999', $phone['phone']);
+ $this->assertTrue($phone['is_billing']);
+ // Contact only has one phone now, so it should be auto-set to primary
+ $this->assertTrue($phone['is_primary']);
+
+ $get = Contact::get(FALSE)
+ ->addWhere('id', '=', $contact['id'])
+ ->addSelect('phone_primary.*')
+ ->addSelect('phone_billing.*')
+ ->execute()->single();
+ $this->assertEquals('99999', $get['phone_primary.phone']);
+ $this->assertEquals('99999', $get['phone_billing.phone']);
+ $this->assertEquals($get['phone_primary.id'], $get['phone_billing.id']);
+ }
+
}
<name>civicrm_saved_search</name>
<comment>Users can save their complex SQL queries and use them later.</comment>
<icon>fa-search-plus</icon>
+ <labelField>label</labelField>
<add>1.1</add>
<field>
<name>id</name>
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'ckeditor4', 'CKEditor4', 'CKEditor4', 'ckeditor4', 1);
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'legacycustomsearches', 'Custom search framework', 'Custom search framework', 'legacycustomsearches', 1);
INSERT IGNORE INTO civicrm_extension (type, full_name, name, label, file, is_active) VALUES ('module', 'org.civicrm.flexmailer', 'FlexMailer', 'FlexMailer', 'flexmailer', 1);
+
+-- dev/core#3783 Recent Items providers
+INSERT INTO civicrm_option_group (`name`, `title`, `is_reserved`, `is_active`) VALUES ('recent_items_providers', {localize}'{ts escape="sql"}Recent Items Providers{/ts}'{/localize}, 1, 1);
+
+SELECT @option_group_id_recent := max(id) from civicrm_option_group where name = 'recent_items_providers';
+
+INSERT INTO civicrm_option_value (`option_group_id`, {localize field='label'}label{/localize}, `value`, `name`, `grouping`, `filter`, `is_default`, `weight`, {localize field='description'}description{/localize}, `is_optgroup`, `is_reserved`, `is_active`, `component_id`, `visibility_id`)
+ VALUES
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Contacts{/ts}'{/localize}, 'Contact', 'Contacts', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Relationships{/ts}'{/localize}, 'Relationship', 'Relationships', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Activities{/ts}'{/localize}, 'Activity', 'Activities', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Notes{/ts}'{/localize}, 'Note', 'Notes', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Groups{/ts}'{/localize}, 'Group', 'Groups', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Cases{/ts}'{/localize}, 'Case', 'Cases', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Contributions{/ts}'{/localize}, 'Contribution', 'Contributions', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Participants{/ts}'{/localize}, 'Participant', 'Participants', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Memberships{/ts}'{/localize}, 'Membership', 'Memberships', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Pledges{/ts}'{/localize}, 'Pledge', 'Pledges', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Events{/ts}'{/localize}, 'Event', 'Events', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL),
+ (@option_group_id_recent, {localize}'{ts escape="sql"}Campaigns{/ts}'{/localize}, 'Campaign', 'Campaigns', NULL, NULL, 0, 1, '', 0, 0, 1, NULL, NULL);