3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2014
37 * This class generates form components generic to all the contact types.
39 * It delegates the work to lower level subclasses and integrates the changes
40 * back in. It also uses a lot of functionality with the CRM API's, so any change
41 * made here could potentially affect the API etc. Be careful, be aware, use unit tests.
44 class CRM_Contact_Form_Contact
extends CRM_Core_Form
{
47 * The contact type of the form
54 * The contact type of the form
58 public $_contactSubType;
61 * The contact id, used when editing the form
68 * The default group id passed in via the url
75 * The default tag id passed in via the url
82 * Name of de-dupe button
86 protected $_dedupeButtonName;
89 * Name of optional save duplicate button
93 protected $_duplicateButtonName;
95 protected $_editOptions = array();
97 protected $_oldSubtypes = array();
101 public $_values = array();
105 public $_customValueCount;
107 * The array of greetings with option group and filed names
114 * Do we want to parse street address.
116 public $_parseStreetAddress;
119 * Check contact has a subtype or not
121 public $_isContactSubType;
124 * Lets keep a cache of all the values that we retrieved.
125 * THis is an attempt to avoid the number of update statements
126 * during the write phase
128 public $_preEditValues;
131 * Build all the data structures needed to build the form
135 public function preProcess() {
136 $this->_action
= CRM_Utils_Request
::retrieve('action', 'String', $this, FALSE, 'add');
138 $this->_dedupeButtonName
= $this->getButtonName('refresh', 'dedupe');
139 $this->_duplicateButtonName
= $this->getButtonName('upload', 'duplicate');
141 $session = CRM_Core_Session
::singleton();
142 if ($this->_action
== CRM_Core_Action
::ADD
) {
143 // check for add contacts permissions
144 if (!CRM_Core_Permission
::check('add contacts')) {
145 CRM_Utils_System
::permissionDenied();
146 CRM_Utils_System
::civiExit();
148 $this->_contactType
= CRM_Utils_Request
::retrieve('ct', 'String',
149 $this, TRUE, NULL, 'REQUEST'
151 if (!in_array($this->_contactType
,
152 array('Individual', 'Household', 'Organization')
155 CRM_Core_Error
::statusBounce(ts('Could not get a contact id and/or contact type'));
158 $this->_isContactSubType
= FALSE;
159 if ($this->_contactSubType
= CRM_Utils_Request
::retrieve('cst', 'String', $this)) {
160 $this->_isContactSubType
= TRUE;
164 $this->_contactSubType
&&
165 !(CRM_Contact_BAO_ContactType
::isExtendsContactType($this->_contactSubType
, $this->_contactType
, TRUE))
167 CRM_Core_Error
::statusBounce(ts("Could not get a valid contact subtype for contact type '%1'", array(1 => $this->_contactType
)));
170 $this->_gid
= CRM_Utils_Request
::retrieve('gid', 'Integer',
171 CRM_Core_DAO
::$_nullObject,
174 $this->_tid
= CRM_Utils_Request
::retrieve('tid', 'Integer',
175 CRM_Core_DAO
::$_nullObject,
178 $typeLabel = CRM_Contact_BAO_ContactType
::contactTypePairs(TRUE, $this->_contactSubType ?
179 $this->_contactSubType
: $this->_contactType
181 $typeLabel = implode(' / ', $typeLabel);
183 CRM_Utils_System
::setTitle(ts('New %1', array(1 => $typeLabel)));
184 $session->pushUserContext(CRM_Utils_System
::url('civicrm/dashboard', 'reset=1'));
185 $this->_contactId
= NULL;
189 if (!$this->_contactId
) {
190 $this->_contactId
= CRM_Utils_Request
::retrieve('cid', 'Positive', $this, TRUE);
193 if ($this->_contactId
) {
195 $params = array('id' => $this->_contactId
);
196 $returnProperities = array('id', 'contact_type', 'contact_sub_type', 'modified_date');
197 CRM_Core_DAO
::commonRetrieve('CRM_Contact_DAO_Contact', $params, $defaults, $returnProperities);
199 if (empty($defaults['id'])) {
200 CRM_Core_Error
::statusBounce(ts('A Contact with that ID does not exist: %1', array(1 => $this->_contactId
)));
203 $this->_contactType
= CRM_Utils_Array
::value('contact_type', $defaults);
204 $this->_contactSubType
= CRM_Utils_Array
::value('contact_sub_type', $defaults);
206 // check for permissions
207 $session = CRM_Core_Session
::singleton();
208 if (!CRM_Contact_BAO_Contact_Permission
::allow($this->_contactId
, CRM_Core_Permission
::EDIT
)) {
209 CRM_Core_Error
::statusBounce(ts('You do not have the necessary permission to edit this contact.'));
212 $displayName = CRM_Contact_BAO_Contact
::displayName($this->_contactId
);
213 $displayName = ts('Edit %1', array(1 => $displayName));
215 // Check if this is default domain contact CRM-10482
216 if (CRM_Contact_BAO_Contact
::checkDomainContact($this->_contactId
)) {
217 $displayName .= ' (' . ts('default organization') . ')';
220 // omitting contactImage from title for now since the summary overlay css doesn't work outside of our crm-container
221 CRM_Utils_System
::setTitle($displayName);
222 $context = CRM_Utils_Request
::retrieve('context', 'String', $this);
223 $qfKey = CRM_Utils_Request
::retrieve('key', 'String', $this);
225 $urlParams = 'reset=1&cid=' . $this->_contactId
;
227 $urlParams .= "&context=$context";
230 if (CRM_Utils_Rule
::qfKey($qfKey)) {
232 $urlParams .= "&key=$qfKey";
235 $session->pushUserContext(CRM_Utils_System
::url('civicrm/contact/view', $urlParams));
237 $values = $this->get('values');
238 // get contact values.
239 if (!empty($values)) {
240 $this->_values
= $values;
244 'id' => $this->_contactId
,
245 'contact_id' => $this->_contactId
,
246 'noRelationships' => TRUE,
251 $contact = CRM_Contact_BAO_Contact
::retrieve($params, $this->_values
, TRUE);
252 $this->set('values', $this->_values
);
256 CRM_Core_Error
::statusBounce(ts('Could not get a contact_id and/or contact_type'));
260 // parse street address, CRM-5450
261 $this->_parseStreetAddress
= $this->get('parseStreetAddress');
262 if (!isset($this->_parseStreetAddress
)) {
263 $addressOptions = CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
266 $this->_parseStreetAddress
= FALSE;
267 if (!empty($addressOptions['street_address']) && !empty($addressOptions['street_address_parsing'])) {
268 $this->_parseStreetAddress
= TRUE;
270 $this->set('parseStreetAddress', $this->_parseStreetAddress
);
272 $this->assign('parseStreetAddress', $this->_parseStreetAddress
);
274 $this->_editOptions
= $this->get('contactEditOptions');
275 if (CRM_Utils_System
::isNull($this->_editOptions
)) {
276 $this->_editOptions
= CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
277 'contact_edit_options', TRUE, NULL,
278 FALSE, 'name', TRUE, 'AND v.filter = 0'
280 $this->set('contactEditOptions', $this->_editOptions
);
283 // build demographics only for Individual contact type
284 if ($this->_contactType
!= 'Individual' &&
285 array_key_exists('Demographics', $this->_editOptions
)
287 unset($this->_editOptions
['Demographics']);
290 // in update mode don't show notes
291 if ($this->_contactId
&& array_key_exists('Notes', $this->_editOptions
)) {
292 unset($this->_editOptions
['Notes']);
295 $this->assign('editOptions', $this->_editOptions
);
296 $this->assign('contactType', $this->_contactType
);
297 $this->assign('contactSubType', $this->_contactSubType
);
299 //build contact subtype form element, CRM-6864
300 $buildContactSubType = TRUE;
301 if ($this->_contactSubType
&& ($this->_action
& CRM_Core_Action
::ADD
)) {
302 $buildContactSubType = FALSE;
304 $this->assign('buildContactSubType', $buildContactSubType);
306 // get the location blocks.
307 $this->_blocks
= $this->get('blocks');
308 if (CRM_Utils_System
::isNull($this->_blocks
)) {
309 $this->_blocks
= CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
310 'contact_edit_options', TRUE, NULL,
311 FALSE, 'name', TRUE, 'AND v.filter = 1'
313 $this->set('blocks', $this->_blocks
);
315 $this->assign('blocks', $this->_blocks
);
317 // this is needed for custom data.
318 $this->assign('entityID', $this->_contactId
);
320 // also keep the convention.
321 $this->assign('contactId', $this->_contactId
);
324 CRM_Contact_Form_Location
::preProcess($this);
326 // retain the multiple count custom fields value
327 if (!empty($_POST['hidden_custom'])) {
328 $customGroupCount = CRM_Utils_Array
::value('hidden_custom_group_count', $_POST);
330 if ($contactSubType = CRM_Utils_Array
::value('contact_sub_type', $_POST)) {
331 $paramSubType = implode(',', $contactSubType);
334 $this->_getCachedTree
= FALSE;
335 unset($customGroupCount[0]);
336 foreach ($customGroupCount as $groupID => $groupCount) {
337 if ($groupCount > 1) {
338 $this->set('groupID', $groupID);
340 for ($i = 0; $i <= $groupCount; $i++
) {
341 CRM_Custom_Form_CustomData
::preProcess($this, NULL, $contactSubType,
342 $i, $this->_contactType
344 CRM_Contact_Form_Edit_CustomData
::buildQuickForm($this);
349 //reset all the ajax stuff, for normal processing
350 if (isset($this->_groupTree
)) {
351 $this->_groupTree
= NULL;
353 $this->set('groupID', NULL);
354 $this->_getCachedTree
= TRUE;
357 // execute preProcess dynamically by js else execute normal preProcess
358 if (array_key_exists('CustomData', $this->_editOptions
)) {
359 //assign a parameter to pass for sub type multivalue
360 //custom field to load
361 if ($this->_contactSubType ||
isset($paramSubType)) {
362 $paramSubType = (isset($paramSubType)) ?
$paramSubType :
363 str_replace(CRM_Core_DAO
::VALUE_SEPARATOR
, ',', trim($this->_contactSubType
, CRM_Core_DAO
::VALUE_SEPARATOR
));
365 $this->assign('paramSubType', $paramSubType);
368 if (CRM_Utils_Request
::retrieve('type', 'String', CRM_Core_DAO
::$_nullObject)) {
369 CRM_Contact_Form_Edit_CustomData
::preProcess($this);
372 $contactSubType = $this->_contactSubType
;
373 // need contact sub type to build related grouptree array during post process
374 if (!empty($_POST['contact_sub_type'])) {
375 $contactSubType = $_POST['contact_sub_type'];
377 //only custom data has preprocess hence directly call it
378 CRM_Custom_Form_CustomData
::preProcess($this, NULL, $contactSubType,
379 1, $this->_contactType
, $this->_contactId
381 $this->assign('customValueCount', $this->_customValueCount
);
387 * Set default values for the form. Note that in edit/view mode
388 * the default values are retrieved from the database
393 public function setDefaultValues() {
394 $defaults = $this->_values
;
397 if ($this->_action
& CRM_Core_Action
::ADD
) {
398 if (array_key_exists('TagsAndGroups', $this->_editOptions
)) {
399 // set group and tag defaults if any
401 $defaults['group'][] = $this->_gid
;
404 $defaults['tag'][$this->_tid
] = 1;
407 if ($this->_contactSubType
) {
408 $defaults['contact_sub_type'] = $this->_contactSubType
;
412 foreach ($defaults['email'] as $dontCare => & $val) {
413 if (isset($val['signature_text'])) {
414 $val['signature_text_hidden'] = $val['signature_text'];
416 if (isset($val['signature_html'])) {
417 $val['signature_html_hidden'] = $val['signature_html'];
421 if (!empty($defaults['contact_sub_type'])) {
422 $defaults['contact_sub_type'] = $this->_oldSubtypes
;
425 // set defaults for blocks ( custom data, address, communication preference, notes, tags and groups )
426 foreach ($this->_editOptions
as $name => $label) {
427 if (!in_array($name, array('Address', 'Notes'))) {
428 $className = 'CRM_Contact_Form_Edit_' . $name;
429 $className::setDefaultValues($this, $defaults);
433 //set address block defaults
434 CRM_Contact_Form_Edit_Address
::setDefaultValues($defaults, $this);
436 if (!empty($defaults['image_URL'])) {
437 list($imageWidth, $imageHeight) = getimagesize(CRM_Utils_String
::unstupifyUrl($defaults['image_URL']));
438 list($imageThumbWidth, $imageThumbHeight) = CRM_Contact_BAO_Contact
::getThumbSize($imageWidth, $imageHeight);
439 $this->assign('imageWidth', $imageWidth);
440 $this->assign('imageHeight', $imageHeight);
441 $this->assign('imageThumbWidth', $imageThumbWidth);
442 $this->assign('imageThumbHeight', $imageThumbHeight);
443 $this->assign('imageURL', $defaults['image_URL']);
446 //set location type and country to default for each block
447 $this->blockSetDefaults($defaults);
449 $this->_preEditValues
= $defaults;
454 * Do the set default related to location type id,
455 * primary location, default country
457 public function blockSetDefaults(&$defaults) {
458 $locationTypeKeys = array_filter(array_keys(CRM_Core_PseudoConstant
::get('CRM_Core_DAO_Address', 'location_type_id')), 'is_int');
459 sort($locationTypeKeys);
461 // get the default location type
462 $locationType = CRM_Core_BAO_LocationType
::getDefault();
464 // unset primary location type
465 $primaryLocationTypeIdKey = CRM_Utils_Array
::key($locationType->id
, $locationTypeKeys);
466 unset($locationTypeKeys[$primaryLocationTypeIdKey]);
468 // reset the array sequence
469 $locationTypeKeys = array_values($locationTypeKeys);
471 // get default phone and im provider id.
472 $defPhoneTypeId = key(CRM_Core_OptionGroup
::values('phone_type', FALSE, FALSE, FALSE, ' AND is_default = 1'));
473 $defIMProviderId = key(CRM_Core_OptionGroup
::values('instant_messenger_service',
474 FALSE, FALSE, FALSE, ' AND is_default = 1'
476 $defWebsiteTypeId = key(CRM_Core_OptionGroup
::values('website_type',
477 FALSE, FALSE, FALSE, ' AND is_default = 1'
480 $allBlocks = $this->_blocks
;
481 if (array_key_exists('Address', $this->_editOptions
)) {
482 $allBlocks['Address'] = $this->_editOptions
['Address'];
485 $config = CRM_Core_Config
::singleton();
486 foreach ($allBlocks as $blockName => $label) {
487 $name = strtolower($blockName);
488 $hasPrimary = $updateMode = FALSE;
490 // user is in update mode.
491 if (array_key_exists($name, $defaults) &&
492 !CRM_Utils_System
::isNull($defaults[$name])
497 for ($instance = 1; $instance <= $this->get($blockName . '_Block_Count'); $instance++
) {
498 // make we require one primary block, CRM-5505
502 CRM_Utils_Array
::value(
504 CRM_Utils_Array
::value($instance, $defaults[$name])
510 //set location to primary for first one.
511 if ($instance == 1) {
513 $defaults[$name][$instance]['is_primary'] = TRUE;
514 $defaults[$name][$instance]['location_type_id'] = $locationType->id
;
517 $locTypeId = isset($locationTypeKeys[$instance - 1]) ?
$locationTypeKeys[$instance - 1] : $locationType->id
;
518 $defaults[$name][$instance]['location_type_id'] = $locTypeId;
521 //set default country
522 if ($name == 'address' && $config->defaultContactCountry
) {
523 $defaults[$name][$instance]['country_id'] = $config->defaultContactCountry
;
526 //set default state/province
527 if ($name == 'address' && $config->defaultContactStateProvince
) {
528 $defaults[$name][$instance]['state_province_id'] = $config->defaultContactStateProvince
;
531 //set default phone type.
532 if ($name == 'phone' && $defPhoneTypeId) {
533 $defaults[$name][$instance]['phone_type_id'] = $defPhoneTypeId;
535 //set default website type.
536 if ($name == 'website' && $defWebsiteTypeId) {
537 $defaults[$name][$instance]['website_type_id'] = $defWebsiteTypeId;
540 //set default im provider.
541 if ($name == 'im' && $defIMProviderId) {
542 $defaults[$name][$instance]['provider_id'] = $defIMProviderId;
547 $defaults[$name][1]['is_primary'] = TRUE;
553 * add the rules (mainly global rules) for form.
554 * All local rules are added near the element
559 public function addRules() {
560 // skip adding formRules when custom data is build
561 if ($this->_addBlockName ||
($this->_action
& CRM_Core_Action
::DELETE
)) {
565 $this->addFormRule(array('CRM_Contact_Form_Edit_' . $this->_contactType
, 'formRule'), $this->_contactId
);
567 // Call Locking check if editing existing contact
568 if ($this->_contactId
) {
569 $this->addFormRule(array('CRM_Contact_Form_Edit_Lock', 'formRule'), $this->_contactId
);
572 if (array_key_exists('Address', $this->_editOptions
)) {
573 $this->addFormRule(array('CRM_Contact_Form_Edit_Address', 'formRule'), $this);
576 if (array_key_exists('CommunicationPreferences', $this->_editOptions
)) {
577 $this->addFormRule(array('CRM_Contact_Form_Edit_CommunicationPreferences', 'formRule'), $this);
582 * Global validation rules for the form
584 * @param array $fields
585 * Posted values of the form.
586 * @param array $errors
587 * List of errors to be posted back to the form.
588 * @param int $contactId
589 * Contact id if doing update.
594 public static function formRule($fields, &$errors, $contactId = NULL) {
595 $config = CRM_Core_Config
::singleton();
598 //1. for each block only single value can be marked as is_primary = true.
599 //2. location type id should be present if block data present.
600 //3. check open id across db and other each block for duplicate.
601 //4. at least one location should be primary.
602 //5. also get primaryID from email or open id block.
604 // take the location blocks.
605 $blocks = CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
606 'contact_edit_options', TRUE, NULL,
607 FALSE, 'name', TRUE, 'AND v.filter = 1'
610 $otherEditOptions = CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
611 'contact_edit_options', TRUE, NULL,
612 FALSE, 'name', TRUE, 'AND v.filter = 0'
614 //get address block inside.
615 if (array_key_exists('Address', $otherEditOptions)) {
616 $blocks['Address'] = $otherEditOptions['Address'];
621 foreach ($blocks as $name => $label) {
622 $hasData = $hasPrimary = array();
623 $name = strtolower($name);
624 if (!empty($fields[$name]) && is_array($fields[$name])) {
625 foreach ($fields[$name] as $instance => $blockValues) {
626 $dataExists = self
::blockDataExists($blockValues);
628 if (!$dataExists && $name == 'address') {
629 $dataExists = CRM_Utils_Array
::value('use_shared_address', $fields['address'][$instance]);
633 // skip remaining checks for website
634 if ($name == 'website') {
638 $hasData[] = $instance;
639 if (!empty($blockValues['is_primary'])) {
640 $hasPrimary[] = $instance;
642 in_array($name, array(
645 )) && !empty($blockValues[$name])
647 $primaryID = $blockValues[$name];
651 if (empty($blockValues['location_type_id'])) {
652 $errors["{$name}[$instance][location_type_id]"] = ts('The Location Type should be set if there is %1 information.', array(1 => $label));
656 if ($name == 'openid' && !empty($blockValues[$name])) {
657 $oid = new CRM_Core_DAO_OpenID();
658 $oid->openid
= $openIds[$instance] = CRM_Utils_Array
::value($name, $blockValues);
659 $cid = isset($contactId) ?
$contactId : 0;
660 if ($oid->find(TRUE) && ($oid->contact_id
!= $cid)) {
661 $errors["{$name}[$instance][openid]"] = ts('%1 already exist.', array(1 => $blocks['OpenID']));
666 if (empty($hasPrimary) && !empty($hasData)) {
667 $errors["{$name}[1][is_primary]"] = ts('One %1 should be marked as primary.', array(1 => $label));
670 if (count($hasPrimary) > 1) {
671 $errors["{$name}[" . array_pop($hasPrimary) . "][is_primary]"] = ts('Only one %1 can be marked as primary.',
678 //do validations for all opend ids they should be distinct.
679 if (!empty($openIds) && (count(array_unique($openIds)) != count($openIds))) {
680 foreach ($openIds as $instance => $value) {
681 if (!array_key_exists($instance, array_unique($openIds))) {
682 $errors["openid[$instance][openid]"] = ts('%1 already used.', array(1 => $blocks['OpenID']));
687 // street number should be digit + suffix, CRM-5450
688 $parseStreetAddress = CRM_Utils_Array
::value('street_address_parsing',
689 CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
693 if ($parseStreetAddress) {
694 if (isset($fields['address']) &&
695 is_array($fields['address'])
697 $invalidStreetNumbers = array();
698 foreach ($fields['address'] as $cnt => $address) {
699 if ($streetNumber = CRM_Utils_Array
::value('street_number', $address)) {
700 $parsedAddress = CRM_Core_BAO_Address
::parseStreetAddress($address['street_number']);
701 if (empty($parsedAddress['street_number'])) {
702 $invalidStreetNumbers[] = $cnt;
707 if (!empty($invalidStreetNumbers)) {
708 $first = $invalidStreetNumbers[0];
709 foreach ($invalidStreetNumbers as & $num) {
710 $num = CRM_Contact_Form_Contact
::ordinalNumber($num);
712 $errors["address[$first][street_number]"] = ts('The street number you entered for the %1 address block(s) is not in an expected format. Street numbers may include numeric digit(s) followed by other characters. You can still enter the complete street address (unparsed) by clicking "Edit Complete Street Address".', array(1 => implode(', ', $invalidStreetNumbers)));
721 * Build the form object
725 public function buildQuickForm() {
726 //load form for child blocks
727 if ($this->_addBlockName
) {
728 $className = 'CRM_Contact_Form_Edit_' . $this->_addBlockName
;
729 return $className::buildQuickForm($this);
732 if ($this->_action
== CRM_Core_Action
::UPDATE
) {
733 $deleteExtra = ts('Are you sure you want to delete contact image.');
735 CRM_Core_Action
::DELETE
=> array(
736 'name' => ts('Delete Contact Image'),
737 'url' => 'civicrm/contact/image',
738 'qs' => 'reset=1&cid=%%id%%&action=delete',
740 'onclick = "if (confirm( \'' . $deleteExtra . '\' ) ) this.href+=\'&confirmed=1\'; else return false;"',
743 $deleteURL = CRM_Core_Action
::formLink($deleteURL,
744 CRM_Core_Action
::DELETE
,
746 'id' => $this->_contactId
,
750 'contact.image.delete',
754 $this->assign('deleteURL', $deleteURL);
757 //build contact type specific fields
758 $className = 'CRM_Contact_Form_Edit_' . $this->_contactType
;
759 $className::buildQuickForm($this);
761 // build Custom data if Custom data present in edit option
762 $buildCustomData = 'noCustomDataPresent';
763 if (array_key_exists('CustomData', $this->_editOptions
)) {
764 $buildCustomData = "customDataPresent";
767 // subtype is a common field. lets keep it here
768 $subtypes = CRM_Contact_BAO_Contact
::buildOptions('contact_sub_type', 'create', array('contact_type' => $this->_contactType
));
769 if (!empty($subtypes)) {
770 $sel = $this->add('select', 'contact_sub_type', ts('Contact Type'),
773 'id' => 'contact_sub_type',
774 'multiple' => 'multiple',
775 'class' => $buildCustomData . ' crm-select2',
780 // build edit blocks ( custom data, demographics, communication preference, notes, tags and groups )
781 foreach ($this->_editOptions
as $name => $label) {
782 if ($name == 'Address') {
783 $this->_blocks
['Address'] = $this->_editOptions
['Address'];
786 if ($name == 'TagsAndGroups') {
789 $className = 'CRM_Contact_Form_Edit_' . $name;
790 $className::buildQuickForm($this);
793 // build tags and groups
794 CRM_Contact_Form_Edit_TagsAndGroups
::buildQuickForm($this, 0, CRM_Contact_Form_Edit_TagsAndGroups
::ALL
,
795 FALSE, NULL, 'Group(s)', 'Tag(s)', NULL, 'select');
797 // build location blocks.
798 CRM_Contact_Form_Edit_Lock
::buildQuickForm($this);
799 CRM_Contact_Form_Location
::buildQuickForm($this);
802 $this->addElement('file', 'image_URL', ts('Browse/Upload Image'), 'size=30 maxlength=60');
803 $this->addUploadElement('image_URL');
805 // add the dedupe button
806 $this->addElement('submit',
807 $this->_dedupeButtonName
,
808 ts('Check for Matching Contact(s)')
810 $this->addElement('submit',
811 $this->_duplicateButtonName
,
812 ts('Save Matching Contact')
814 $this->addElement('submit',
815 $this->getButtonName('next', 'sharedHouseholdDuplicate'),
816 ts('Save With Duplicate Household')
822 'name' => ts('Save'),
827 if (CRM_Core_Permission
::check('add contacts')) {
830 'name' => ts('Save and New'),
831 'spacing' => ' ',
837 'name' => ts('Cancel'),
840 if (!empty($this->_values
['contact_sub_type'])) {
841 $this->_oldSubtypes
= explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
842 trim($this->_values
['contact_sub_type'], CRM_Core_DAO
::VALUE_SEPARATOR
)
845 $this->assign('oldSubtypes', json_encode($this->_oldSubtypes
));
847 $this->addButtons($buttons);
851 * Form submission of new/edit contact is processed.
856 public function postProcess() {
857 // check if dedupe button, if so return.
858 $buttonName = $this->controller
->getButtonName();
859 if ($buttonName == $this->_dedupeButtonName
) {
863 //get the submitted values in an array
864 $params = $this->controller
->exportValues($this->_name
);
866 $group = CRM_Utils_Array
::value('group', $params);
867 if (!empty($group) && is_array($group)) {
868 unset($params['group']);
869 foreach ($group as $key => $value) {
870 $params['group'][$value] = 1;
874 CRM_Contact_BAO_Contact_Optimizer
::edit($params, $this->_preEditValues
);
876 if (!empty($params['image_URL'])) {
877 CRM_Contact_BAO_Contact
::processImageParams($params);
880 if (is_numeric(CRM_Utils_Array
::value('current_employer_id', $params)) && !empty($params['current_employer'])) {
881 $params['current_employer'] = $params['current_employer_id'];
884 // don't carry current_employer_id field,
885 // since we don't want to directly update DAO object without
886 // handling related business logic ( eg related membership )
887 if (isset($params['current_employer_id'])) {
888 unset($params['current_employer_id']);
891 $params['contact_type'] = $this->_contactType
;
892 if (empty($params['contact_sub_type']) && $this->_isContactSubType
) {
893 $params['contact_sub_type'] = array($this->_contactSubType
);
896 if ($this->_contactId
) {
897 $params['contact_id'] = $this->_contactId
;
900 //make deceased date null when is_deceased = false
901 if ($this->_contactType
== 'Individual' && !empty($this->_editOptions
['Demographics']) && empty($params['is_deceased'])) {
902 $params['is_deceased'] = FALSE;
903 $params['deceased_date'] = NULL;
906 if (isset($params['contact_id'])) {
907 // process membership status for deceased contact
908 $deceasedParams = array(
909 'contact_id' => CRM_Utils_Array
::value('contact_id', $params),
910 'is_deceased' => CRM_Utils_Array
::value('is_deceased', $params, FALSE),
911 'deceased_date' => CRM_Utils_Array
::value('deceased_date', $params, NULL),
913 $updateMembershipMsg = $this->updateMembershipStatus($deceasedParams);
916 // action is taken depending upon the mode
917 if ($this->_action
& CRM_Core_Action
::UPDATE
) {
918 CRM_Utils_Hook
::pre('edit', $params['contact_type'], $params['contact_id'], $params);
921 CRM_Utils_Hook
::pre('create', $params['contact_type'], NULL, $params);
924 $customFields = CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, TRUE);
927 //if subtype is set, send subtype as extend to validate subtype customfield
928 $customFieldExtends = (CRM_Utils_Array
::value('contact_sub_type', $params)) ?
$params['contact_sub_type'] : $params['contact_type'];
930 $params['custom'] = CRM_Core_BAO_CustomField
::postProcess($params,
936 if ($this->_contactId
&& !empty($this->_oldSubtypes
)) {
937 CRM_Contact_BAO_ContactType
::deleteCustomSetForSubtypeMigration($this->_contactId
,
938 $params['contact_type'],
940 $params['contact_sub_type']
944 if (array_key_exists('CommunicationPreferences', $this->_editOptions
)) {
945 // this is a chekbox, so mark false if we dont get a POST value
946 $params['is_opt_out'] = CRM_Utils_Array
::value('is_opt_out', $params, FALSE);
949 // process shared contact address.
950 CRM_Contact_BAO_Contact_Utils
::processSharedAddress($params['address']);
952 if (!array_key_exists('TagsAndGroups', $this->_editOptions
) && !empty($params['group'])) {
953 unset($params['group']);
956 if (!empty($params['contact_id']) && ($this->_action
& CRM_Core_Action
::UPDATE
) && !empty($params['group'])) {
957 // figure out which all groups are intended to be removed
958 $contactGroupList = CRM_Contact_BAO_GroupContact
::getContactGroup($params['contact_id'], 'Added');
959 if (is_array($contactGroupList)) {
960 foreach ($contactGroupList as $key) {
961 if ((!array_key_exists($key['group_id'], $params['group']) ||
$params['group'][$key['group_id']] != 1) && empty($key['is_hidden'])) {
962 $params['group'][$key['group_id']] = -1;
968 // parse street address, CRM-5450
969 $parseStatusMsg = NULL;
970 if ($this->_parseStreetAddress
) {
971 $parseResult = self
::parseAddress($params);
972 $parseStatusMsg = self
::parseAddressStatusMsg($parseResult);
975 // Allow un-setting of location info, CRM-5969
976 $params['updateBlankLocInfo'] = TRUE;
978 $contact = CRM_Contact_BAO_Contact
::create($params, TRUE, FALSE, TRUE);
981 if ($this->_contactId
) {
982 $message = ts('%1 has been updated.', array(1 => $contact->display_name
));
985 $message = ts('%1 has been created.', array(1 => $contact->display_name
));
988 // set the contact ID
989 $this->_contactId
= $contact->id
;
991 if (array_key_exists('TagsAndGroups', $this->_editOptions
)) {
992 //add contact to tags
993 CRM_Core_BAO_EntityTag
::create($params['tag'], 'civicrm_contact', $params['contact_id']);
996 if (isset($params['contact_taglist']) && !empty($params['contact_taglist'])) {
997 CRM_Core_Form_Tag
::postProcess($params['contact_taglist'], $params['contact_id'], 'civicrm_contact', $this);
1001 if (!empty($parseStatusMsg)) {
1002 $message .= "<br />$parseStatusMsg";
1004 if (!empty($updateMembershipMsg)) {
1005 $message .= "<br />$updateMembershipMsg";
1008 $session = CRM_Core_Session
::singleton();
1009 $session->setStatus($message, ts('Contact Saved'), 'success');
1011 // add the recently viewed contact
1012 $recentOther = array();
1013 if (($session->get('userID') == $contact->id
) ||
1014 CRM_Contact_BAO_Contact_Permission
::allow($contact->id
, CRM_Core_Permission
::EDIT
)
1016 $recentOther['editUrl'] = CRM_Utils_System
::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contact->id
);
1019 if (($session->get('userID') != $this->_contactId
) && CRM_Core_Permission
::check('delete contacts')) {
1020 $recentOther['deleteUrl'] = CRM_Utils_System
::url('civicrm/contact/view/delete', 'reset=1&delete=1&cid=' . $contact->id
);
1023 CRM_Utils_Recent
::add($contact->display_name
,
1024 CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $contact->id
),
1026 $this->_contactType
,
1028 $contact->display_name
,
1032 // here we replace the user context with the url to view this contact
1033 $buttonName = $this->controller
->getButtonName();
1034 if ($buttonName == $this->getButtonName('upload', 'new')) {
1035 $resetStr = "reset=1&ct={$contact->contact_type}";
1036 $resetStr .= $this->_contactSubType ?
"&cst={$this->_contactSubType}" : '';
1037 $session->replaceUserContext(CRM_Utils_System
::url('civicrm/contact/add', $resetStr));
1040 $context = CRM_Utils_Request
::retrieve('context', 'String', $this);
1041 $qfKey = CRM_Utils_Request
::retrieve('key', 'String', $this);
1042 //validate the qfKey
1043 $urlParams = 'reset=1&cid=' . $contact->id
;
1045 $urlParams .= "&context=$context";
1047 if (CRM_Utils_Rule
::qfKey($qfKey)) {
1048 $urlParams .= "&key=$qfKey";
1051 $session->replaceUserContext(CRM_Utils_System
::url('civicrm/contact/view', $urlParams));
1054 // now invoke the post hook
1055 if ($this->_action
& CRM_Core_Action
::UPDATE
) {
1056 CRM_Utils_Hook
::post('edit', $params['contact_type'], $contact->id
, $contact);
1059 CRM_Utils_Hook
::post('create', $params['contact_type'], $contact->id
, $contact);
1064 * Is there any real significant data in the hierarchical location array
1066 * @param array $fields
1067 * The hierarchical value representation of this location.
1070 * true if data exists, false otherwise
1072 public static function blockDataExists(&$fields) {
1073 if (!is_array($fields)) {
1077 static $skipFields = array(
1086 foreach ($fields as $name => $value) {
1088 foreach ($skipFields as $skip) {
1089 if (strpos("[$skip]", $name) !== FALSE) {
1090 if ($name == 'phone') {
1100 if (is_array($value)) {
1101 if (self
::blockDataExists($value)) {
1106 if (!empty($value)) {
1116 * That checks for duplicate contacts
1118 * @param array $fields
1119 * Fields array which are submitted.
1121 * @param int $contactID
1123 * @param string $contactType
1126 public static function checkDuplicateContacts(&$fields, &$errors, $contactID, $contactType) {
1127 // if this is a forced save, ignore find duplicate rule
1128 if (empty($fields['_qf_Contact_upload_duplicate'])) {
1130 $dedupeParams = CRM_Dedupe_Finder
::formatParams($fields, $contactType);
1131 $ids = CRM_Dedupe_Finder
::dupesByParams($dedupeParams, $contactType, 'Supervised', array($contactID));
1134 $contactLinks = CRM_Contact_BAO_Contact_Utils
::formatContactIDSToLinks($ids, TRUE, TRUE, $contactID);
1136 $duplicateContactsLinks = '<div class="matching-contacts-found">';
1137 $duplicateContactsLinks .= ts('One matching contact was found. ', array(
1138 'count' => count($contactLinks['rows']),
1139 'plural' => '%count matching contacts were found.<br />',
1141 if ($contactLinks['msg'] == 'view') {
1142 $duplicateContactsLinks .= ts('You can View the existing contact', array(
1143 'count' => count($contactLinks['rows']),
1144 'plural' => 'You can View the existing contacts',
1148 $duplicateContactsLinks .= ts('You can View or Edit the existing contact', array(
1149 'count' => count($contactLinks['rows']),
1150 'plural' => 'You can View or Edit the existing contacts',
1153 if ($contactLinks['msg'] == 'merge') {
1154 // We should also get a merge link if this is for an existing contact
1155 $duplicateContactsLinks .= ts(', or Merge this contact with an existing contact');
1157 $duplicateContactsLinks .= '.';
1158 $duplicateContactsLinks .= '</div>';
1159 $duplicateContactsLinks .= '<table class="matching-contacts-actions">';
1161 for ($i = 0; $i < count($contactLinks['rows']); $i++
) {
1163 $row .= ' <td class="matching-contacts-name"> ';
1164 $row .= $contactLinks['rows'][$i]['display_name'];
1166 $row .= ' <td class="matching-contacts-email"> ';
1167 $row .= $contactLinks['rows'][$i]['primary_email'];
1169 $row .= ' <td class="action-items"> ';
1170 $row .= $contactLinks['rows'][$i]['view'];
1171 $row .= $contactLinks['rows'][$i]['edit'];
1172 $row .= CRM_Utils_Array
::value('merge', $contactLinks['rows'][$i]);
1177 $duplicateContactsLinks .= $row . '</table>';
1178 $duplicateContactsLinks .= ts("If you're sure this record is not a duplicate, click the 'Save Matching Contact' button below.");
1180 $errors['_qf_default'] = $duplicateContactsLinks;
1182 // let smarty know that there are duplicates
1183 $template = CRM_Core_Smarty
::singleton();
1184 $template->assign('isDuplicate', 1);
1186 elseif (!empty($fields['_qf_Contact_refresh_dedupe'])) {
1187 // add a session message for no matching contacts
1188 CRM_Core_Session
::setStatus(ts('No matching contact found.'), ts('None Found'), 'info');
1194 * Use the form name to create the tpl file name
1198 public function getTemplateFileName() {
1199 if ($this->_contactSubType
) {
1200 $templateFile = "CRM/Contact/Form/Edit/SubType/{$this->_contactSubType}.tpl";
1201 $template = CRM_Core_Form
::getTemplate();
1202 if ($template->template_exists($templateFile)) {
1203 return $templateFile;
1206 return parent
::getTemplateFileName();
1210 * Parse all address blocks present in given params
1211 * and return parse result for all address blocks,
1212 * This function either parse street address in to child
1213 * elements or build street address from child elements.
1215 * @param array $params
1216 * of key value consist of address blocks.
1219 * as array of sucess/fails for each address block
1221 public function parseAddress(&$params) {
1222 $parseSuccess = $parsedFields = array();
1223 if (!is_array($params['address']) ||
1224 CRM_Utils_System
::isNull($params['address'])
1226 return $parseSuccess;
1229 foreach ($params['address'] as $instance => & $address) {
1230 $buildStreetAddress = FALSE;
1231 $parseFieldName = 'street_address';
1237 if (!empty($address[$fld])) {
1238 $parseFieldName = 'street_number';
1239 $buildStreetAddress = TRUE;
1244 // main parse string.
1245 $parseString = CRM_Utils_Array
::value($parseFieldName, $address);
1247 // parse address field.
1248 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($parseString);
1250 if ($buildStreetAddress) {
1251 //hack to ignore spaces between number and suffix.
1252 //here user gives input as street_number so it has to
1253 //be street_number and street_number_suffix, but
1254 //due to spaces though preg detect string as street_name
1255 //consider it as 'street_number_suffix'.
1256 $suffix = $parsedFields['street_number_suffix'];
1258 $suffix = $parsedFields['street_name'];
1260 $address['street_number_suffix'] = $suffix;
1261 $address['street_number'] = $parsedFields['street_number'];
1263 $streetAddress = NULL;
1266 'street_number_suffix',
1270 if (in_array($fld, array(
1274 $streetAddress .= ' ';
1276 $streetAddress .= CRM_Utils_Array
::value($fld, $address);
1278 $address['street_address'] = trim($streetAddress);
1279 $parseSuccess[$instance] = TRUE;
1283 // consider address is automatically parseable,
1284 // when we should found street_number and street_name
1285 if (empty($parsedFields['street_name']) ||
empty($parsedFields['street_number'])) {
1289 // check for original street address string.
1290 if (empty($parseString)) {
1294 $parseSuccess[$instance] = $success;
1296 // we do not reset element values, but keep what we've parsed
1297 // in case of partial matches: CRM-8378
1299 // merge parse address in to main address block.
1300 $address = array_merge($address, $parsedFields);
1304 return $parseSuccess;
1308 * Check parse result and if some address block fails then this
1309 * function return the status message for all address blocks.
1311 * @param array $parseResult
1312 * An array of address blk instance and its status.
1314 * @return null|string
1315 * $statusMsg string status message for all address blocks.
1317 public static function parseAddressStatusMsg($parseResult) {
1319 if (!is_array($parseResult) ||
empty($parseResult)) {
1323 $parseFails = array();
1324 foreach ($parseResult as $instance => $success) {
1326 $parseFails[] = self
::ordinalNumber($instance);
1330 if (!empty($parseFails)) {
1331 $statusMsg = ts("Complete street address(es) have been saved. However we were unable to split the address in the %1 address block(s) into address elements (street number, street name, street unit) due to an unrecognized address format. You can set the address elements manually by clicking 'Edit Address Elements' next to the Street Address field while in edit mode.",
1332 array(1 => implode(', ', $parseFails))
1340 * Convert normal number to ordinal number format.
1341 * like 1 => 1st, 2 => 2nd and so on...
1343 * @param int $number
1344 * number to convert in to ordinal number.
1347 * ordinal number for given number.
1349 public static function ordinalNumber($number) {
1350 if (empty($number)) {
1355 switch (floor($number / 10) %
10) {
1358 switch ($number %
10) {
1373 return "$number$str";
1377 * Update membership status to deceased
1378 * function return the status message for updated membership.
1380 * @param array $deceasedParams
1381 * having contact id and deceased value.
1383 * @return null|string
1384 * $updateMembershipMsg string status message for updated membership.
1386 public function updateMembershipStatus($deceasedParams) {
1387 $updateMembershipMsg = NULL;
1388 $contactId = CRM_Utils_Array
::value('contact_id', $deceasedParams);
1389 $deceasedDate = CRM_Utils_Array
::value('deceased_date', $deceasedParams);
1391 // process to set membership status to deceased for both active/inactive membership
1393 $this->_contactType
== 'Individual' && !empty($deceasedParams['is_deceased'])
1396 $session = CRM_Core_Session
::singleton();
1397 $userId = $session->get('userID');
1399 $userId = $contactId;
1402 // get deceased status id
1403 $allStatus = CRM_Member_PseudoConstant
::membershipStatus();
1404 $deceasedStatusId = array_search('Deceased', $allStatus);
1405 if (!$deceasedStatusId) {
1406 return $updateMembershipMsg;
1410 if ($deceasedDate && strtotime($deceasedDate) > $today) {
1411 return $updateMembershipMsg;
1414 // get non deceased membership
1415 $dao = new CRM_Member_DAO_Membership();
1416 $dao->contact_id
= $contactId;
1417 $dao->whereAdd("status_id != $deceasedStatusId");
1419 $activityTypes = CRM_Core_PseudoConstant
::activityType(TRUE, FALSE, FALSE, 'name');
1420 $allStatus = CRM_Member_PseudoConstant
::membershipStatus();
1422 while ($dao->fetch()) {
1423 // update status to deceased (for both active/inactive membership )
1424 CRM_Core_DAO
::setFieldValue('CRM_Member_DAO_Membership', $dao->id
,
1425 'status_id', $deceasedStatusId
1428 // add membership log
1429 $membershipLog = array(
1430 'membership_id' => $dao->id
,
1431 'status_id' => $deceasedStatusId,
1432 'start_date' => CRM_Utils_Date
::isoToMysql($dao->start_date
),
1433 'end_date' => CRM_Utils_Date
::isoToMysql($dao->end_date
),
1434 'modified_id' => $userId,
1435 'modified_date' => date('Ymd'),
1436 'membership_type_id' => $dao->membership_type_id
,
1437 'max_related' => $dao->max_related
,
1440 CRM_Member_BAO_MembershipLog
::add($membershipLog, CRM_Core_DAO
::$_nullArray);
1442 //create activity when membership status is changed
1443 $activityParam = array(
1444 'subject' => "Status changed from {$allStatus[$dao->status_id]} to {$allStatus[$deceasedStatusId]}",
1445 'source_contact_id' => $userId,
1446 'target_contact_id' => $dao->contact_id
,
1447 'source_record_id' => $dao->id
,
1448 'activity_type_id' => array_search('Change Membership Status', $activityTypes),
1452 'activity_date_time' => date('Y-m-d H:i:s'),
1454 'is_current_revision' => 1,
1457 $activityResult = civicrm_api('activity', 'create', $activityParam);
1464 $updateMembershipMsg = ts("%1 Current membership(s) for this contact have been set to 'Deceased' status.",
1465 array(1 => $memCount)
1470 return $updateMembershipMsg;