diff --git a/CRM/I3val/Handler/ContactCustomUpdate.php b/CRM/I3val/Handler/ContactCustomUpdate.php new file mode 100644 index 0000000..91e1810 --- /dev/null +++ b/CRM/I3val/Handler/ContactCustomUpdate.php @@ -0,0 +1,375 @@ +<?php +/*-------------------------------------------------------+ +| Ilja's Input Validation Extension | +| Amnesty International Vlaanderen | +| Copyright (C) 2019 SYSTOPIA | +| Author: B. Endres (endres@systopia.de) | +| http://www.systopia.de/ | ++--------------------------------------------------------+ +| This program is released as free software under the | +| Affero GPL license. You can redistribute it and/or | +| modify it under the terms of this license which you | +| can read by viewing the included agpl.txt or online | +| at www.gnu.org/licenses/agpl.html. Removal of this | +| copyright header is strictly prohibited without | +| written permission from the original author(s). | ++--------------------------------------------------------*/ + +use CRM_I3val_ExtensionUtil as E; + +/** + * this class will handle performing the changes + * that are passed on from the API call + */ +class CRM_I3val_Handler_ContactCustomUpdate extends CRM_I3val_ActivityHandler { + + public static $group_name = 'i3val_contact_custom_updates'; + public static $field2label = NULL; + + public function getField2Label() { + if (self::$field2label === NULL) { + self::$field2label = ['value' => E::ts('Value')]; + } + return self::$field2label; + } + + /** + * get the main key/identifier for this handler + */ + public function getKey() { + return 'contact'; + } + + /** + * get a human readable name for this handler + */ + public function getName() { + return E::ts("Contact Custom Fields Update"); + } + + /** + * returns a list of CiviCRM entities this handler can process + */ + public function handlesEntities() { + return array('Contact'); + } + + /** + * get the list of + */ + public function getFields() { + $field2label = $this->getField2Label(); + return array_keys($field2label); + } + + /** + * Get the JSON specification file defining the custom group used for this data + */ + public function getCustomGroupSpeficationFiles() { + return array(__DIR__ . '/../../../resources/contact_custom_updates_custom_group.json'); + } + + /** + * Get the custom group name + */ + public function getCustomGroupName() { + return self::$group_name; + } + + /** + * Verify whether the changes make sense + * + * @return array $key -> error message + */ + public function verifyChanges($activity, $values, $objects = array()) { + // TODO: check? + return array(); + } + + /** + * Apply the changes + * + * @return array with changes to the activity + */ + public function applyChanges($activity, $values, $objects = array()) { + $contact = $objects['contact']; + $activity_update = array(); + if (!$this->hasData($activity)) { + // NO DATA, no updates + return $activity_update; + } + + // calculate generic update + $contact_update = array(); + $this->applyUpdateData($contact_update, $values, '%s', "%s_applied"); + + // remove the ones that are not flagged as 'apply' + foreach (array_keys($contact_update) as $key) { + $apply_key = "{$key}_apply"; + if (!isset($values[$apply_key]) || !strlen($values[$apply_key])) { + unset($contact_update[$key]); + } + } + $this->applyUpdateData($activity_update, $contact_update, self::$group_name . '.%s_applied', '%s'); + + // execute update + if (!empty($contact_update)) { + $contact_update['id'] = $contact['id']; + $this->resolveFields($contact_update); + $this->resolvePreferredLanguageToLabel($contact_update, FALSE); + CRM_I3val_Session::log("UPDATE contact " . json_encode($contact_update)); + civicrm_api3('Contact', 'create', $contact_update); + } + + return $activity_update; + } + + + /** + * Load and assign necessary data to the form + */ + public function renderActivityData($activity, $form) { + $config = CRM_I3val_Configuration::getConfiguration(); + $field2label = self::getField2Label(); + $values = $this->compileValues(self::$group_name, $field2label, $activity); + $this->resolvePreferredLanguageToLabel($form->contact); + $this->addCurrentValues($values, $form->contact); + + // exceptions for current values + if (isset($values['prefix']) && !empty($form->contact['individual_prefix'])) { + $values['prefix']['current'] = $form->contact['individual_prefix']; + } + if (isset($values['suffix']) && !empty($form->contact['individual_suffix'])) { + $values['suffix']['current'] = $form->contact['individual_suffix']; + } + + // create input fields and apply checkboxes + $active_fields = array(); + $checkbox_fields = array(); // these will be displayed as checkboxes rather than strings + + foreach ($field2label as $fieldname => $fieldlabel) { + // if there is no values, omit field + if ($config->clearingFieldsAllowed()) { + if (empty($values[$fieldname]['submitted']) && empty($values[$fieldname]['original'])) { + continue; + } + } else { + if (!isset($values[$fieldname]['submitted']) || !strlen($values[$fieldname]['submitted'])) { + continue; + } + } + + // this field has data: + $active_fields[$fieldname] = $fieldlabel; + + // generate input field + if (in_array($fieldname, array('prefix', 'suffix', 'gender'))) { + // add the text input + $form->add( + 'select', + "{$fieldname}_applied", + $fieldlabel, + $this->getOptionList($fieldname) + ); + + } elseif ($fieldname == 'birth_date' || $fieldname == 'deceased_date') { + $form->addDate( + "{$fieldname}_applied", + $fieldlabel, + FALSE, + array('formatType' => 'activityDate') + ); + + // format date (drop time) + if (isset($values[$fieldname]['submitted'])) { + $values[$fieldname]['submitted'] = substr($values[$fieldname]['submitted'], 0, 10); + } + if (isset($values[$fieldname]['original'])) { + $values[$fieldname]['original'] = substr($values[$fieldname]['original'], 0, 10); + } + + } elseif ($fieldname == 'preferred_language') { + $form->add( + 'select', + "{$fieldname}_applied", + $fieldlabel, + $this->getOptionValueList('languages', 'label', E::ts("none")) + ); + + + + } elseif (substr($fieldname, 0, 3) == 'do_' || substr($fieldname, 0, 3) == 'is_') { + $checkbox_fields[$fieldname] = 1; + $form->add( + 'checkbox', + "{$fieldname}_applied", + $fieldlabel + ); + + } else { + // add the text input + $form->add( + 'text', + "{$fieldname}_applied", + $fieldlabel + ); + } + + if (!empty($values[$fieldname]['applied'])) { + $form->setDefaults(array("{$fieldname}_applied" => $values[$fieldname]['applied'])); + } else { + $form->setDefaults(array("{$fieldname}_applied" => isset($values[$fieldname]['submitted']) ? $values[$fieldname]['submitted'] : '')); + } + + // add the apply checkbox + $form->add( + 'checkbox', + "{$fieldname}_apply", + $fieldlabel + ); + $form->setDefaults(array("{$fieldname}_apply" => 1)); + } + + $form->assign('i3val_contact_fields', $field2label); + $form->assign('i3val_contact_values', $values); + $form->assign('i3val_active_contact_fields', $active_fields); + $form->assign('i3val_active_contact_checkboxes', $checkbox_fields); + } + + /** + * Get the path of the template rendering the form + */ + public function getTemplate() { + return 'CRM/I3val/Handler/ContactUpdate.tpl'; + } + + /** + * Calculate the data to be created and add it to the $activity_data Activity.create params + * @todo specify + */ + public function generateDiffData($entity, $submitted_data, &$activity_data) { + if ($entity != 'Contact') { + throw new Exception("Can only process Contact entity"); + } + + // make sure the custom fields are in the 'custom_xx' format + CRM_I3val_CustomData::resolveCustomFields($submitted_data); + + // get all custom fields + $submitted_custom_data = []; + $custom_field_ids = []; + foreach ($submitted_data as $field_name => $field_data) { + if (preg_match('/^custom_(?<custom_field_id>[0-9]+)$/', $field_name, $match)) { + $submitted_custom_data[$field_name] = $field_data; + $custom_field_ids[] = $match['custom_field_id']; + } + } + + if (!empty($submitted_custom_data)) { + // load that custom data + $current_custom_data = []; + $custom_data_query = civicrm_api3('CustomValue', 'get', [ + 'option.limit' => 0, + 'entity_id' => $submitted_data['id'], + 'entity_table' => 'civicrm_contact']); + foreach ($custom_data_query['values'] as $field_id => $field_data) { + $current_custom_data["custom_{$field_id}"] = $field_data['latest']; + } + + // resolve / diff + $change_index = 1; + foreach ($custom_field_ids as $field_id) { + $submitted_value = CRM_Utils_Array::value("custom_{$field_id}", $submitted_custom_data, ''); + $current_value = CRM_Utils_Array::value("custom_{$field_id}", $current_custom_data, ''); + if ($this->differs($field_id, $submitted_value, $current_value)) { + // there is a difference -> add a record + $custom_id_field = CRM_I3val_CustomData::getCustomFieldKey(self::$group_name, 'custom_field_id'); + $original_field = CRM_I3val_CustomData::getCustomFieldKey(self::$group_name, 'value_original'); + $submitted_field = CRM_I3val_CustomData::getCustomFieldKey(self::$group_name, 'value_submitted'); + $activity_data["{$custom_id_field}:-{$change_index}"] = $field_id; + $activity_data["{$original_field}:-{$change_index}"] = $current_value; + $activity_data["{$submitted_field}:-{$change_index}"] = $submitted_value; + $change_index++; + } + } + } + } + + /** + * Check whether the presented values differ in the context of the given custom field + * + * @param $field_id integer custom field id + * @param $value1 mixed value 1 + * @param $value2 mixed value 2 + * @return boolean + */ + protected function differs($field_id, $value1, $value2) { + // todo: implement + return $value1 != $value2; + } + + /** + * Resolve the text field names (e.g. 'gender') + * to their ID representations ('gender_id'). + */ + protected function resolveFields(&$data, $add_default = FALSE) { + parent::resolveFields($data, $add_default); + $this->resolveOptionValueField($data, 'gender', 'gender', 'gender_id'); + $this->resolveOptionValueField($data, 'individual_prefix', 'prefix', 'prefix_id'); + $this->resolveOptionValueField($data, 'individual_suffix', 'suffix', 'suffix_id'); + } + + + /** + * Get dropdown lists + */ + protected function getOptionList($fieldname) { + $option_group_name = NULL; + + switch ($fieldname) { + case 'gender': + return $this->getOptionValueList('gender', 'label', E::ts('none')); + + case 'prefix': + return $this->getOptionValueList('individual_prefix', 'label', E::ts('none')); + + case 'suffix': + return $this->getOptionValueList('individual_suffix', 'label', E::ts('none')); + + default: + return $this->getOptionValueList($fieldname); + } + } + + /** + * Since the brilliant preferred_language field has no *_id + * counterpart, we are forced to decide whether we want to + * store the key or the label. The API needs the ID, while + * our activities will store the label + */ + protected function resolvePreferredLanguageToLabel(&$data, $to_label = TRUE) { + if (!empty($data['preferred_language'])) { + if ($to_label) { + // we want to make sure that the label is used... + if (preg_match("#^[a-z]{2}_[A-Z]{2}$#", $data['preferred_language'])) { + // ... but it is the key -> find the right label + $languages = $this->getOptionValues('languages', 'name'); + if (isset($languages[$data['preferred_language']])) { + $data['preferred_language'] = $languages[$data['preferred_language']]['label']; + } else { + // not found + unset($data['preferred_language']); + } + } + + } else { + // we want to make sure that the key is there... + if (!preg_match("#^[a-z]{2}_[A-Z]{2}$#", $data['preferred_language'])) { + // ...but it seems like it isn't -> find best match + $option_value = $this->getMatchingOptionValue('languages', $data['preferred_language'], TRUE, 'name'); + $data['preferred_language'] = $option_value['name']; + } + } + } + } +} diff --git a/CRM/I3val/Logic.php b/CRM/I3val/Logic.php index b61c80b..b6d8081 100644 --- a/CRM/I3val/Logic.php +++ b/CRM/I3val/Logic.php @@ -65,15 +65,46 @@ public static function createEntityUpdateRequest($entity, $params) { $activity_data['activity_type_id'] = $activity_type_id; // create activity, reload and return - CRM_I3val_Session::log('ACTIVIY ' . json_encode($activity_data)); - CRM_I3val_CustomData::resolveCustomFields($activity_data); - $activity = civicrm_api3('Activity', 'create', $activity_data); - return civicrm_api3('Activity', 'getsingle', array('id' => $activity['id'])); + return self::createActivity($activity_data); } else { return NULL; } } + /** + * Create a new activity, along with the custom fields: + * + * @param $activity_data array activity data + * @return array created activity data += */ + protected static function createActivity($activity_data) { + //CRM_I3val_Session::log('ACTIVIY ' . json_encode($activity_data)); + + // first: resolve the (remaining) parameters to the custom_xx notation + CRM_I3val_CustomData::resolveCustomFields($activity_data); + + // separate out the parameters + $activity_base_data = []; + $activity_custom_data = []; + foreach ($activity_data as $field_name => $field_value) { + if (preg_match("/^custom_[0-9]/", $field_name)) { + $activity_custom_data[$field_name] = $field_value; + } else { + $activity_base_data[$field_name] = $field_value; + } + } + + // create the activity itself + $activity = civicrm_api3('Activity', 'create', $activity_base_data); + + // create the custom data + if (!empty($activity_custom_data)) { + $activity_custom_data['entity_id'] = $activity['id']; + civicrm_api3('CustomValue', 'create', $activity_custom_data); + } + + return civicrm_api3('Activity', 'getsingle', ['id' => $activity['id']]); + } /** * Add the generic activity parameters, partly derived from the $params diff --git a/resources/contact_custom_updates_custom_group.json b/resources/contact_custom_updates_custom_group.json new file mode 100644 index 0000000..5fd23be --- /dev/null +++ b/resources/contact_custom_updates_custom_group.json @@ -0,0 +1,75 @@ +{ + "_lookup": ["name"], + "_translate": ["title"], + "name": "i3val_contact_custom_updates", + "title": "Contact Custom Fields Update", + "extends": "Activity", + "style": "Inline", + "collapse_display": "1", + "is_active": "1", + "is_multiple": "1", + "table_name": "civicrm_value_i3val_contact_custom_updates", + "collapse_adv_display": "1", + "is_reserved": "0", + "_fields": [ + { + "_lookup": ["column_name", "custom_group_id"], + "_translate": ["label"], + "name": "custom_field_id", + "column_name": "custom_field_id", + "label": "Custom Field ID", + "data_type": "Integer", + "html_type": "Text", + "is_required": "0", + "is_searchable": "1", + "is_search_range": "1", + "is_active": "1", + "is_view": "1" + }, + { + "_lookup": ["column_name", "custom_group_id"], + "_translate": ["label"], + "name": "value_original", + "column_name": "value_original", + "label": "Value (original)", + "data_type": "String", + "html_type": "Text", + "text_length": "255", + "is_required": "0", + "is_searchable": "1", + "is_search_range": "1", + "is_active": "1", + "is_view": "1" + }, + { + "_lookup": ["column_name", "custom_group_id"], + "_translate": ["label"], + "name": "value_submitted", + "column_name": "value_submitted", + "label": "Value (submitted)", + "data_type": "String", + "html_type": "Text", + "text_length": "255", + "is_required": "0", + "is_searchable": "1", + "is_search_range": "1", + "is_active": "1", + "is_view": "1" + }, + { + "_lookup": ["column_name", "custom_group_id"], + "_translate": ["label"], + "name": "value_applied", + "column_name": "value_applied", + "label": "Value (applied)", + "data_type": "String", + "html_type": "Text", + "text_length": "255", + "is_required": "0", + "is_searchable": "1", + "is_search_range": "1", + "is_active": "1", + "is_view": "1" + } + ] +} \ No newline at end of file