Linked profile custom field

In the profile type editor it is possible to create a custom field that links to a different profile type (see attachment). I would like to be able to create a backend custom field for the groups editor that works the same way, but what I read in this article: Adding a Custom Field to the Back-end seems to indicate that is not possible (looking at the options for the ‘type’ attribute). Just checking to see whether anyone knows if there is a way to do this…

Thanks,

-= G =-

I believe I’ve done something recently that may achieve what you’re looking for.

I rebuilt our catalog output (for printing) this year, and in doing so updated the data structures which make it happen. In the end, I ended up making the following linked fields:

  • Catalog Sections: Related Group, taken from a list of existing groups in LiveWhale.
  • Catalog Sections: Linked Courses (x4), taken from a list of course codes in data from our SIS.
  • Requirements: Related Sections (x4), taken from the existing Catalog Sections in LiveWhale.

A Related Group field filled with groups which exist in LiveWhale.

A Related Group field's HTML, showing group ids as the option values and group names as the option text.


The solution I arrived at requires a custom module and a little development work. Be warned, it is also a bit “hacky”, but it appears to work so long as there’s only one short string to save per field. In brief:

  1. In a profile type, edit the type and add a new custom profile field(s) with the type “text (one line)”. Save and note the custom field’s id.
  2. In a custom module, define new behavior with the “onOutput” handler function.
  3. In the function, start by using $LW->read() to pull the data you want to include in the field.
  4. Modify the data as needed and, for each option, determine what will be the value (the entity’s id) and display text (the entity’s name).
  5. Search the buffer for the field(s) to edit and replace their text <input/> with a <select/> element which includes all of the options.

Since I was doing this for nine fields, I wrote a utility function to handle the replacement step (#5):

/*
	Utility function to replace a profile custom field, type text, into a <select/> field.
	 - $buffer is the variable that can be modified in certain LW handlers, like onOutput();
	 - $options is expected to be an array of value->name pairs to build the select <options/>.
	 - $field_ids is expected to be an array where the values are custom field ids to alter.
	 - Return: the edited $buffer variable
*/
function replaceTextWithSelect($buffer, $options, $field_ids) {

	/*	This attempts to update a custom text field in a profile with a <select/> field using given id->name pairs.
		In the profile type editor, the field should be a <input type="text"/> field.
		This will replace the <input/> with a <select/> element, with the value being saved and recalled.
	*/

	// build our new set of options
	$select_options = '<option value=""></option>'; // default, empty
	foreach ($options as $value => $name) { 
		$select_options .= '<option value="'.$value.'">'.$name.'</option>'."\n";
	}

	// for each field to edit
	foreach($field_ids as $field_id) {

		// identify the current value of the field, if it has one
		// $match[0] is whole <input/>, $match [1] is value number only, no finds should be NULL
		$match = array();
		preg_match('/<input type="text".*id="profiles_custom_'.$field_id.'".*value="(.*)".*>/U', $buffer, $match, PREG_UNMATCHED_AS_NULL);
		$current = empty($match) ? -1 : $match[1]; // avoids "undefined offset" if no match found

		// in the field's own copy of the options, mark the current option if there is one
		$regex_search = '/(<option value="'.$current.'")(>.*<\/option>)/U';
		$regex_replace = '$1 selected="selected"$2';
		$field_options = preg_replace($regex_search, $regex_replace, $select_options);

		// replace the custom field <input> with the <select/> and options
		$regex_search = '/<input type="text".*id="profiles_custom_'.$field_id.'".*>/U';
		$regex_replace = '<select class="form-control was-text" name="custom_'.$field_id.'" id="profiles_custom_'.$field_id.'">'.$field_options.'</select>';
		$buffer = preg_replace($regex_search, $regex_replace, $buffer);

	}

	// return our edited $buffer
	return $buffer;

}

Then, for each profile type and set of its fields that share the same data, code like the following can be defined within the “onOutput” handler:

/*
	Editing profiles of the Catalog Section type (55).
	 - Replace profile custom text field with a select with group value->name pairs.
*/
if ($_LW->page=='profiles_edit' && $_LW->_GET['tid']==55) {

	// configuration variables
	$field_ids = [310]; // Related Group field

	// get all groups
	$data_type = 'groups';
	$args= ['paginate' => 999];
	$groups = $_LW->read($data_type, $args);
	$groups = (isset($groups['results'])) ? $groups['results'] : $groups; // API change?

	// for each group, build an array of value->name pairs
	$options = [];
	foreach ($groups as $group) {
		$gid = $group['id'];
		$gname = $group['title'];
		// optional: further filter/edit the per item data as needed
		$options[$gid] = $gname;
	}

	// sort the list by name
	natcasesort($options);

	// call helper function to make the edits to the buffer
	$buffer = replaceTextWithSelect($buffer, $options, $field_ids);

}

Some notes I have from our module:

  • Wanted to avoid JS solutions, if possible.
  • Client custom fields are appended after the onOutput handler, meaning we can’t use them here. This functionality may be limited to only custom profile fields.
  • Making a text field a select field is odd, but the underlying text field (or database column) appears willing to store and recall our similar input of a single string (the selected option’s value). In the end, the change is practically cosmetic.
  • We avoid using profile custom field types with “pre-defined” values (select, radio, etc), which are prone to either being hard to read (id) or prone to desync (name).

Hope that helps you and others. Also hope this solution isn’t too sacrilegious, lol.

Let me know if you have any questions (or see anything to improve).

ah - tricky! I see your point, and it may work for us. Thanks so much for taking the time to explain it!

-= G =-

1 Like

oh - looking at this more closely, I note what you said about client custom fields - that is what I need to do because I need the field to be in the group editor, so maybe this won’t work for me…

UPDATE: I looked at the $buffer anyway, and I am seeing what I need to change, so I’m going to go ahead and see if I can make this work…

In my case, I initially had Requirements as blurbs and Sections as profiles. I attempted to try and modify a client custom field in the blurb type, but I kept running into a mistiming, something like the <input> not yet existing in the buffer when my code was trying to replace it. I don’t think I looked at the exact buffer; just the end result of no change and assumed timing.

I knew it worked in the profile type, which I did first, so I decided to migrate the Requirements blurbs to a new profile type to take advantage of the environment that was working.

If you get an approach like this to work using client custom fields, I’d be curious to know what I might have missed. I had been wanting a way to make use of these “relationship” fields for a while, and while having them for profiles is helpful, the ability to use them for “anything” would be great.

I’m partway into it and have some optimism but my debug log seems to have failed, so I’m waiting for help from LW

Well I did get this working on a group edit screen. Here’s my use case, for context: We have a faculty & staff profile type available to groups. I wanted to have a contact name specified for each group, and that name should be drawn from the profiles that exist in the group, if any. So I have a custom field called custom_program_contact_name. In that field I actually want to store the id of the person’s profile, but display the name, and also to use the id in widgets elsewhere to fish out the name and url of the contact person for each group when needed.

So here’s my code for private.application.group_contact.php:

<?php $_LW->REGISTERED_APPS['group_contact']=array( 'title'=>'Group Contact', 'handlers'=>array('onOutput'), ); class LiveWhaleApplicationGroupContact { public function onOutput($buffer) { global $_LW; // if on the group editor page if ($_LW->page == 'groups_edit') { // check for existing id in field $match = array(); preg_match('//U', $buffer, $match, PREG_UNMATCHED_AS_NULL); $current = empty($match) ? -1 : $match[1]; // avoids "undefined offset" if no match found // get group name from form since gid isn't working $match = array(); preg_match('/"Enter a name for this group.*value="(.*)".*>/U', $buffer, $match, PREG_UNMATCHED_AS_NULL); $group_name = empty($match) ? -1 : $match[1]; // setup search and read people profiles from group // $group_id = $_LW->_GET['id']; $data_type = 'profiles'; $args = ['tid' => 2, 'group' => $group_name]; $people = $_LW->read($data_type, $args); $options = array(); foreach($people as $person) { $options[$person['id']] = $person['name']; } natcasesort($options); $select_options = ''; foreach($options as $value => $name) { if ($value == $current) { $select_options .= '' . $name . ''; } else { $select_options .= '' . $name . ''; } } // substitute the for the text field $regex_search = '//U'; $regex_replace = '' . $select_options . ''; $buffer = preg_replace($regex_search, $regex_replace, $buffer); return $buffer; } else { return $buffer; } } } ?>

Some notes:
Since I only have one custom field to deal with, I didn’t need the separate function so everything happens in the main procedure.

I also check first to see if there is a value in the custom field so I have that when building the select list.

Finally, for some reason I’m not able to use gid, which is easily found in $_GET[‘id’], in the read() call since it doesn’t filter the way I expect it to, so instead I have a hacky preg_match() to find the group name in the actual form on the page, so I can call ‘group’ instead if ‘gid’! It does work, though, so all’s well that ends well, I guess.

As far as I can tell, the same approach ought to work for other custom variables, and I have a couple other applications in mind.

Thank you very much for your idea and examples, as this has solved my problem, and I wouldn’t have thought of that approach!

Best,

-= G =-

oh sorry - my code didn’t come out nicely formatted like yours even though it went in that way - how did you get that to work?

Nice work.

There’s a WYSIWYG editor button for a script block.

I expanded the code to read it, but it seems like parts were stripped from your copy/paste, specifically any HTML tags. Some examples below; I’ll leave it to you to share the full code.

// nothing in REGEX
preg_match('//U', $buffer, $match, PREG_UNMATCHED_AS_NULL); $current = empty($match) ? -1 : $match[1];
// should be <option/> I think
$select_options .= '' . $name . '';
// should be <select/> I think
$regex_replace = '' . $select_options . '';

It would be nice to use the gid, both to avoid group name changes and skip a regex/parse. Likely not a major concern in this specific case given we’re editing the group itself, except perhaps if the name and coordinator are changed in the same edit/save?

Still, if you have the gid, perhaps a $_LW->read() with a filter argument would work?

// untested
$group_id = $_LW->_GET['id'];
$data_type = 'profiles';
$args = [
	'tid' => 2,
	'filter' => 'gid|equals|'.$group_id
];
$people = $_LW->read($data_type, $args);

LW said that gid would never work because it is not a widget argument, and suggested the filter approach as well. Here’s the updated code:

<?php

$_LW->REGISTERED_APPS['group_contact']=array(
		'title'=>'Group Contact',
		'handlers'=>array('onOutput'),
	);

class LiveWhaleApplicationGroupContact
{
	public function onOutput($buffer)
	{
		global $_LW;

		// if on the group editor page
		if ($_LW->page == 'groups_edit')
		{
			// check for existing id in field
			$match = array();
			preg_match('/<input type="text".*id="custom_program_contact_name".*value="(.*)".*>/U', $buffer, $match, PREG_UNMATCHED_AS_NULL);
			$current = empty($match) ? -1 : $match[1]; // avoids "undefined offset" if no match found

			// get group name from form since gid isn't working
/* 			$match = array();
			preg_match('/"Enter a name for this group.*value="(.*)".*>/U', $buffer, $match, PREG_UNMATCHED_AS_NULL);
			$group_name = empty($match) ? -1 : $match[1]; */

			// setup search and read people profiles from group
			$data_type	= 'profiles';
//			$args		= ['tid' => 2, 'group' => $group_name];
			$args		= ['tid' => 2, 'filter' => 'gid|equals|' . $_LW->_GET['id']];
			$people 	= $_LW->read($data_type, $args);

			$options = array();
			foreach($people as $person)
			{
				$options[$person['id']] = $person['name'];
			}
			natcasesort($options);

			$select_options = '<option value=""></option>';
			foreach($options as $value => $name)
			{
				if ($value == $current)
				{
					$select_options .= '<option value="' . $value . '" selected="selected">' . $name . '</option>';
				}
				else
				{
					$select_options .= '<option value="' . $value . '">' . $name . '</option>';
				}
			}

			// substitute the <select> for the text field
			$regex_search = '/<input type="text".*id="custom_program_contact_name".*>/U';
			$regex_replace = '<select class="form-control was-text" name="custom_program_contact_name" id="profiles_custom_program_contact_name">' . $select_options . '</select>';
			$buffer = preg_replace($regex_search, $regex_replace, $buffer);

			return $buffer;
		}
		else
		{
			return $buffer;
		}
	}
}
?>

Looks great to me, and glad I could help. Not sure what I missed in my attempt, though.

If I can find time (or another need), perhaps I can generalize all of this into a single custom module able to help set up both client custom fields and profile custom fields.

Since it was top of mind, I decided to spend some personal time this evening trying to make a generalized custom module for this functionality. I want to give it another review and test later, so I’ll share it hereafter.


@gccervone One observation for your implementation:

// setup search and read people profiles from group
$data_type	= 'profiles';
$args		= ['tid' => 2, 'filter' => 'gid|equals|' . $_LW->_GET['id']];
$people 	= $_LW->read($data_type, $args);

Like gid, tid doesn’t seem to be a valid widget argument. I tried using your code in an example, and the $_LW->read was giving me all profiles in the group.

If you want just profiles of a single type, then the following should work:

$data_type	= 'profiles';
$args		= [
				'filter' => [
					'tid|equals|2',
					'gid|equals|'.$_LW->_GET['id']
				]
			];
$people		= $_LW->read($data_type, $args);

In the example based on your use case, I assumed that the coordinator could either be faculty or staff, which Beloit has as two separate types. Thus, for the example, I used this approach in order to get both types at once.

$data_type	= 'profiles';
$args		= [
				'type' => ['Faculty','Staff'],
				'filter' => 'gid|equals|'.$_LW->_GET['id']
			];
$people		= $_LW->read($data_type, $args);

Aside: If interested more than one profiles by tid, using just filter in a single $_LW->read isn’t possible, I think. Filter mode is either “match all” or “match any”, neither of which work for a mixed request like “group and (faculty or staff)”. Would either have to use to the type argument– which is fine for types like employee profiles given the name should never change– or make multiple reads with different arguments and merge the results.

That is super-interesting. For me, tid does work fine and only pulls the profiles that I want. I never thought to check whether that was a valid parameter because I was using it before I found out about gid not working. Your point is well taken however, and it is probably wiser to use the filter approach. We don’t have the separate types for faculty and staff so that part is not an issue for us.

Thanks,

-= G =-

Right, the generalized custom module, commented like crazy and with examples:

@gccervone I noted you as a co-author for confirming it can work on custom fields. Let me know if you have any preferences regarding that.

1 Like

oh great - I’ll give this a try as soon as I get a chance.

You really did all the work - thank you for the courtesy but I don’t need any credit…

1 Like