Skip to content

Commit

Permalink
Merge pull request #23296 from colemanw/afformRelationships
Browse files Browse the repository at this point in the history
Afform - support relationships
  • Loading branch information
seamuslee001 authored May 9, 2022
2 parents aac03e2 + e1f7995 commit b6c6efb
Show file tree
Hide file tree
Showing 13 changed files with 223 additions and 29 deletions.
3 changes: 2 additions & 1 deletion CRM/Contact/DAO/Relationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* Generated from xml/schema/CRM/Contact/Relationship.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:138d23f70bbc1e8c3b1ad2d247e9a8df)
* (GenCodeChecksum:3c7051137838e12caf53b96b8c369f2b)
*/

/**
Expand Down Expand Up @@ -238,6 +238,7 @@ public static function &fields() {
'localizable' => 0,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'html' => [
'type' => 'EntityRef',
'label' => ts("Contact A"),
],
'add' => '1.1',
Expand Down
20 changes: 18 additions & 2 deletions ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ public static function getFields($entityName, $params = []) {
$params['values']['state_province_id'] = \Civi::settings()->get('defaultContactStateProvince');
}
$fields = (array) civicrm_api4($entityName, 'getFields', $params);

// Add implicit joins to search fields
if ($params['action'] === 'get') {
foreach (array_reverse($fields, TRUE) as $index => $field) {
Expand All @@ -122,7 +121,24 @@ public static function getFields($entityName, $params = []) {
}
}
}
return array_column($fields, NULL, 'name');
// Index by name
$fields = array_column($fields, NULL, 'name');
// Mix in alterations declared by afform entities
if ($params['action'] === 'create') {
$afEntity = self::getMetadata()['entities'][$entityName] ?? [];
if (!empty($afEntity['alterFields'])) {
foreach ($afEntity['alterFields'] as $fieldName => $changes) {
// Allow field to be deleted
if ($changes === FALSE) {
unset($fields[$fieldName]);
}
else {
$fields[$fieldName] = \CRM_Utils_Array::crmArrayMerge($changes, ($fields[$fieldName] ?? []));
}
}
}
}
return $fields;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ public function fields() {
'name' => 'blocks',
'data_type' => 'Array',
],
[
'name' => 'entities',
'data_type' => 'Array',
],
[
'name' => 'fields',
'data_type' => 'Array',
Expand Down
14 changes: 14 additions & 0 deletions ext/afform/admin/afformEntities/Relationship.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
return [
'type' => 'primary',
'defaults' => "{
security: 'FBAC'
}",
'icon' => 'fa-handshake-o',
'boilerplate' => FALSE,
'repeatable' => FALSE,
'alterFields' => [
'contact_id_a' => ['input_attrs' => ['multiple' => TRUE]],
'contact_id_b' => ['input_attrs' => ['multiple' => TRUE]],
],
];
28 changes: 15 additions & 13 deletions ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,21 @@
var pos = 1 + _.findLastIndex(editor.layout['#children'], {'#tag': 'af-entity'});
editor.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
// Create a new af-fieldset container for the entity
var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element);
fieldset['af-fieldset'] = type + num;
fieldset['af-title'] = meta.label + ' ' + num;
// Add boilerplate contents
_.each(meta.boilerplate, function (tag) {
fieldset['#children'].push(tag);
});
// Attempt to place the new af-fieldset after the last one on the form
pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset');
if (pos) {
editor.layout['#children'].splice(pos, 0, fieldset);
} else {
editor.layout['#children'].push(fieldset);
if (meta.boilerplate !== false) {
var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element);
fieldset['af-fieldset'] = type + num;
fieldset['af-title'] = meta.label + ' ' + num;
// Add boilerplate contents
_.each(meta.boilerplate, function (tag) {
fieldset['#children'].push(tag);
});
// Attempt to place the new af-fieldset after the last one on the form
pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset');
if (pos) {
editor.layout['#children'].splice(pos, 0, fieldset);
} else {
editor.layout['#children'].push(fieldset);
}
}
delete $scope.entities[type + num].loading;
if (selectTab) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@
};

$scope.isRepeatable = function() {
return ctrl.node['af-fieldset'] || (block.directive && afGui.meta.blocks[block.directive].repeat) || ctrl.join;
return ctrl.join ||
(block.directive && afGui.meta.blocks[block.directive].repeat) ||
(ctrl.node['af-fieldset'] && ctrl.editor.getEntityDefn(ctrl.editor.getEntity(ctrl.node['af-fieldset'])) !== false);
};

this.toggleRepeat = function() {
Expand Down
13 changes: 10 additions & 3 deletions ext/afform/core/Civi/Afform/Event/AfformSubmitEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* If special processing for an entity type is desired, add a new listener with a higher priority
* than 0, and do one of two things:
*
* 1. Fully process the save, and cancel event propagation to bypass `processGenericEntity`.
* 2. Manipulate the $records and allow the default listener to perform the save.
* Setting $record['fields'] = NULL will cancel saving a record, e.g. if the record is not valid.
* 1. Fully process the save, and call `$event->stopPropagation()` to skip `processGenericEntity`.
* 2. Manipulate the $records and allow `processGenericEntity` to perform the save.
* Setting $record['fields'] = NULL will prevent saving a record, e.g. if the record is not valid.
*
* @package Civi\Afform\Event
*/
Expand Down Expand Up @@ -88,6 +88,13 @@ public function getEntityName(): string {
return $this->entityName;
}

/**
* @return array{type: string, fields: array, joins: array, security: string, actions: array}
*/
public function getEntity() {
return $this->getFormDataModel()->getEntity($this->entityName);
}

/**
* @return callable
* API4-style
Expand Down
18 changes: 14 additions & 4 deletions ext/afform/core/Civi/Afform/FormDataModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
*/
class FormDataModel {

protected $defaults = ['security' => 'RBAC', 'actions' => ['create' => TRUE, 'update' => TRUE]];
protected $defaults = [
'security' => 'RBAC',
'actions' => ['create' => TRUE, 'update' => TRUE],
'min' => 1,
'max' => 1,
];

/**
* @var array[]
Expand Down Expand Up @@ -136,9 +141,14 @@ protected function parseFields($nodes, $entity = NULL, $join = NULL, $searchDisp
if (!is_array($node) || !isset($node['#tag'])) {
continue;
}
elseif (isset($node['af-fieldset']) && !empty($node['#children'])) {
$searchDisplay = $node['af-fieldset'] ? NULL : $this->findSearchDisplay($node);
$this->parseFields($node['#children'], $node['af-fieldset'], $join, $searchDisplay);
elseif (isset($node['af-fieldset'])) {
$entity = $node['af-fieldset'] ?? NULL;
$searchDisplay = $entity ? NULL : $this->findSearchDisplay($node);
if ($entity && isset($node['af-repeat'])) {
$this->entities[$entity]['min'] = $node['min'] ?? 0;
$this->entities[$entity]['max'] = $node['max'] ?? NULL;
}
$this->parseFields($node['#children'] ?? [], $node['af-fieldset'], $join, $searchDisplay);
}
elseif ($searchDisplay && $node['#tag'] === 'af-field') {
$this->searchDisplays[$searchDisplay]['fields'][$node['name']] = AHQ::getProps($node);
Expand Down
69 changes: 66 additions & 3 deletions ext/afform/core/Civi/Api4/Action/Afform/Submit.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Civi\Afform\Event\AfformSubmitEvent;
use Civi\Api4\AfformSubmission;
use Civi\Api4\RelationshipType;
use Civi\Api4\Utils\CoreUtil;

/**
Expand All @@ -28,7 +29,6 @@ protected function processForm() {
$entityValues = [];
foreach ($this->_formDataModel->getEntities() as $entityName => $entity) {
$entityValues[$entityName] = [];

// Gather submitted field values from $values['fields'] and sub-entities from $values['joins']
foreach ($this->values[$entityName] ?? [] as $values) {
// Only accept values from fields on the form
Expand All @@ -47,8 +47,12 @@ protected function processForm() {
}
$entityValues[$entityName][] = $values;
}
// Predetermined values override submitted values
if (!empty($entity['data'])) {
// If no submitted values but data exists, fill the minimum number of records
for ($index = 0; $index < $entity['min']; $index++) {
$entityValues[$entityName][$index] = $entityValues[$entityName][$index] ?? ['fields' => []];
}
// Predetermined values override submitted values
foreach ($entityValues[$entityName] as $index => $vals) {
$entityValues[$entityName][$index]['fields'] = $entity['data'] + $vals['fields'];
}
Expand Down Expand Up @@ -94,7 +98,8 @@ private function replaceReferences($entityName, $records) {
if (is_array($value)) {
foreach ($value as $i => $val) {
if (in_array($val, $entityNames, TRUE)) {
$records[$key]['fields'][$field][$i] = $this->_entityIds[$val][0]['id'] ?? NULL;
$refIds = array_filter(array_column($this->_entityIds[$val], 'id'));
array_splice($records[$key]['fields'][$field], $i, 1, $refIds);
}
}
}
Expand Down Expand Up @@ -162,6 +167,64 @@ public static function processGenericEntity(AfformSubmitEvent $event) {
}
}

/**
* @param \Civi\Afform\Event\AfformSubmitEvent $event
*/
public static function processRelationships(AfformSubmitEvent $event) {
if ($event->getEntityType() !== 'Relationship') {
return;
}
// Prevent processGenericEntity
$event->stopPropagation();
$api4 = $event->getSecureApi4();
$relationship = $event->records[0]['fields'] ?? [];
if (empty($relationship['contact_id_a']) || empty($relationship['contact_id_b']) || empty($relationship['relationship_type_id'])) {
return;
}
$relationshipType = RelationshipType::get(FALSE)
->addWhere('id', '=', $relationship['relationship_type_id'])
->execute()->single();
$isReciprocal = $relationshipType['label_a_b'] == $relationshipType['label_b_a'];
$isActive = !isset($relationship['is_active']) || !empty($relationship['is_active']);
// Each contact id could be multivalued (e.g. using `af-repeat`)
foreach ((array) $relationship['contact_id_a'] as $contact_id_a) {
foreach ((array) $relationship['contact_id_b'] as $contact_id_b) {
$params = $relationship;
$params['contact_id_a'] = $contact_id_a;
$params['contact_id_b'] = $contact_id_b;
// Check for existing relationships (if allowed)
if (!empty($event->getEntity()['actions']['update'])) {
$where = [
['is_active', '=', $isActive],
['relationship_type_id', '=', $relationship['relationship_type_id']],
];
// Reciprocal relationship types need an extra check
if ($isReciprocal) {
$where[] = ['OR',
['AND', ['contact_id_a', '=', $contact_id_a], ['contact_id_b', '=', $contact_id_b]],
['AND', ['contact_id_a', '=', $contact_id_b], ['contact_id_b', '=', $contact_id_a]],
];
}
else {
$where[] = ['contact_id_a', '=', $contact_id_a];
$where[] = ['contact_id_b', '=', $contact_id_b];
}
$existing = $api4('Relationship', 'get', ['where' => $where])->first();
if ($existing) {
$params['id'] = $existing['id'];
unset($params['contact_id_a'], $params['contact_id_b']);
// If this is a flipped reciprocal relationship, also flip the permissions
$params['is_permission_a_b'] = $relationship['is_permission_b_a'] ?? NULL;
$params['is_permission_b_a'] = $relationship['is_permission_a_b'] ?? NULL;
}
}
$api4('Relationship', 'save', [
'records' => [$params],
]);
}
}
}

/**
* This saves joins (sub-entities) such as Email, Address, Phone, etc.
*
Expand Down
1 change: 1 addition & 0 deletions ext/afform/core/afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function afform_civicrm_config(&$config) {
$dispatcher = Civi::dispatcher();
$dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processGenericEntity'], 0);
$dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'preprocessContact'], 10);
$dispatcher->addListener('civi.afform.submit', ['\Civi\Api4\Action\Afform\Submit', 'processRelationships'], 1);
$dispatcher->addListener('hook_civicrm_angularModules', ['\Civi\Afform\AngularDependencyMapper', 'autoReq'], -1000);
$dispatcher->addListener('hook_civicrm_alterAngular', ['\Civi\Afform\AfformMetadataInjector', 'preprocess']);
$dispatcher->addListener('hook_civicrm_check', ['\Civi\Afform\StatusChecks', 'hook_civicrm_check']);
Expand Down
12 changes: 10 additions & 2 deletions ext/afform/core/tests/phpunit/Civi/Afform/FormDataModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function getEntityExamples() {
];

$cases[] = [
'html' => '<af-form><af-entity type="Foo" name="foobar"/><div af-fieldset="foobar"><af-field name="propA" /><span><p><af-field name="propB" defn="{title: \'Whiz\'}" /></p></span></div></af-form>',
'html' => '<af-form><af-entity type="Foo" name="foobar"/><div af-fieldset="foobar" af-repeat><af-field name="propA" /><span><p><af-field name="propB" defn="{title: \'Whiz\'}" /></p></span></div></af-form>',
'entities' => [
'foobar' => [
'type' => 'Foo',
Expand All @@ -51,6 +51,8 @@ public function getEntityExamples() {
'joins' => [],
'security' => 'RBAC',
'actions' => ['create' => 1, 'update' => 1],
'min' => 0,
'max' => NULL,
],
],
];
Expand All @@ -65,6 +67,8 @@ public function getEntityExamples() {
'joins' => [],
'security' => 'RBAC',
'actions' => ['create' => 1, 'update' => 1],
'min' => 1,
'max' => 1,
],
'whiz_bang' => [
'type' => 'Whiz',
Expand All @@ -73,12 +77,14 @@ public function getEntityExamples() {
'joins' => [],
'security' => 'RBAC',
'actions' => ['create' => 1, 'update' => 1],
'min' => 1,
'max' => 1,
],
],
];

$cases[] = [
'html' => '<af-form><div><af-entity type="Foo" name="foobar" security="FBAC" actions="{create: false, update: true}"/></div></af-form>',
'html' => '<af-form><div><af-entity type="Foo" name="foobar" security="FBAC" actions="{create: false, update: true}"/><div af-fieldset="foobar" af-repeat min="1" max="2"></div></div></af-form>',
'entities' => [
'foobar' => [
'type' => 'Foo',
Expand All @@ -87,6 +93,8 @@ public function getEntityExamples() {
'joins' => [],
'security' => 'FBAC',
'actions' => ['create' => FALSE, 'update' => TRUE],
'min' => 1,
'max' => 2,
],
],
];
Expand Down
Loading

0 comments on commit b6c6efb

Please sign in to comment.