diff --git a/includes/og.field.inc b/includes/og.field.inc new file mode 100644 index 0000000..2f96b94 --- /dev/null +++ b/includes/og.field.inc @@ -0,0 +1,348 @@ + t('OG reference'), + 'description' => t('Complex widget to reference groups.'), + 'field types' => array('entityreference'), + ); + + return $widgets; +} + + +/** + * Implements hook_field_widget_form(). + */ +function og_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + $entity_type = $instance['entity_type']; + $entity = isset($element['#entity']) ? $element['#entity'] : NULL; + + if (!$entity) { + return; + } + + if ($field['settings']['handler'] != 'og') { + $params = array('%label' => $instance['label']); + form_error($form, t('Field %label is a group-audience but its Entity selection mode is not defined as "Organic groups" in the field settings page.', $params)); + return; + } + + // Cache the processed entity, to make sure we call the widget only once. + $cache = &drupal_static(__FUNCTION__, array()); + list($id,, $bundle) = entity_extract_ids($entity_type, $entity); + $field_name = $field['field_name']; + + $identifier = $field_name . ':' . $entity_type . ':' . $id; + if (isset($cache[$identifier])) { + return; + } + $cache[$identifier] = TRUE; + + ctools_include('fields'); + + $field_modes = array('default'); + $has_admin = FALSE; + + // The group IDs that might not be accessible by the user, but we need + // to keep even after saving. + $element['#other_groups_ids'] = array(); + $element['#element_validate'][] = 'og_complex_widget_element_validate'; + + if (user_access('administer group')) { + $has_admin = TRUE; + $field_modes[] = 'admin'; + } + + // Get the "Other group" group IDs. + $target_type = $field['settings']['target_type']; + $entity_gids = og_get_entity_groups($entity_type, $entity); + $entity_gids = !empty($entity_gids[$target_type]) ? $entity_gids[$target_type] : array(); + + $user_gids = og_get_entity_groups(); + $user_gids = !empty($user_gids[$target_type]) ? $user_gids[$target_type] : array(); + + $other_groups_ids = array(); + $full_diff = array_diff(array_merge($entity_gids, $user_gids), array_intersect($entity_gids, $user_gids)); + $other_groups_ids = array_values($full_diff); + + foreach ($field_modes as $field_mode) { + $mocked_instance = og_get_mocked_instance($instance, $field_mode); + $dummy_entity = clone $entity; + + if ($has_admin) { + $mocked_instance['required'] = FALSE; + $mocked_instance['label'] = $field_mode == 'default' ? t('Default') : t('Administrator'); + + if ($id) { + // The field might be required, and it will throw an exception + // when we try to set an empty value, so change the wrapper's + // info. + $wrapper = entity_metadata_wrapper($entity_type, $dummy_entity, array('property info alter' => 'og_property_info_alter', 'field name' => $field_name)); + if ($field_mode == 'admin') { + // Keep only the hidden group IDs on the entity, so they won't + // appear again on the "admin" field, for example on an autocomplete + // widget type. + $valid_ids = $other_groups_ids ? entityreference_get_selection_handler($field, $mocked_instance, $entity_type, $dummy_entity)->validateReferencableEntities($other_groups_ids) : array(); + $valid_ids = $field['cardinality'] == 1 ? reset($valid_ids) : $valid_ids; + $wrapper->{$field_name}->set($valid_ids ? $valid_ids : NULL); + } + else { + // Keep only the groups that belong to the user and to the entity. + $my_group_ids = array_values(array_intersect($user_gids, $entity_gids)); + $valid_ids = $my_group_ids ? entityreference_get_selection_handler($field, $mocked_instance, $entity_type, $dummy_entity)->validateReferencableEntities($my_group_ids) : array(); + + $valid_ids = $field['cardinality'] == 1 ? reset($valid_ids) : $valid_ids; + $wrapper->{$field_name}->set($valid_ids ? $valid_ids : NULL); + } + } + } + else { + // Non-admin user. + $mocked_instance_other_groups = $mocked_instance; + $mocked_instance_other_groups['field_mode'] = 'admin'; + if ($other_groups_ids && $valid_ids = entityreference_get_selection_handler($field, $mocked_instance_other_groups, $entity_type, $dummy_entity)->validateReferencableEntities($other_groups_ids)) { + foreach ($valid_ids as $id) { + $element['#other_groups_ids'][] = array('target_id' => $id); + } + } + } + + $dummy_form_state = $form_state; + if (empty($form_state['rebuild'])) { + // Form is "fresh" (i.e. not call from field_add_more_submit()), so + // re-set the items-count, to show the correct amount for the mocked + // instance. + $dummy_form_state['field'][$field_name][LANGUAGE_NONE]['items_count'] = !empty($dummy_entity->{$field_name}[LANGUAGE_NONE]) ? count($dummy_entity->{$field_name}[LANGUAGE_NONE]) : 0; + } + + $new_element = ctools_field_invoke_field($mocked_instance, 'form', $entity_type, $dummy_entity, $form, $dummy_form_state, array('default' => TRUE)); + $element[$field_mode] = $new_element[$field_name][LANGUAGE_NONE]; + + if (in_array($mocked_instance['widget']['type'], array('entityreference_autocomplete', 'entityreference_autocomplete_tags'))) { + // Change the "Add more" button name so it adds only the needed + // element. + if (!empty($element[$field_mode]['add_more']['#name'])) { + $element[$field_mode]['add_more']['#name'] .= '__' . $field_mode; + } + + foreach (array_keys($element[$field_mode]) as $delta) { + if (!is_numeric($delta)) { + continue; + } + + $sub_element = &$element[$field_mode][$delta]['target_id']; + + // Rebuild the autocomplete path. + $path = explode('/', $sub_element['#autocomplete_path']); + $sub_element['#autocomplete_path'] = 'og/autocomplete'; + + // Add autocomplete type + $sub_element['#autocomplete_path'] .= "/$path[2]/$path[3]/$path[4]/$path[5]"; + + // Add field mode. + $sub_element['#autocomplete_path'] .= "/$field_mode"; + + // Add the entity ID. + $sub_element['#autocomplete_path'] .= "/$path[6]"; + if (!empty($path[7])) { + // Add the text. + $sub_element['#autocomplete_path'] .= "/$path[7]"; + } + } + } + } + + $form['#after_build']['og'] = 'og_complex_widget_after_build'; + + return $element; +} + +/** + * Property info alter; Change mocked field to be non-required. + */ +function og_property_info_alter($wrapper, $info) { + $property_info = $wrapper->info(); + $field_name = $property_info['field name']; + $info['properties'][$field_name]['required'] = FALSE; + return $info; +} + +/** + * Helper function; Get the mocked instance. + */ +function og_get_mocked_instance($instance, $field_mode) { + $mocked_instance = $instance; + $widget_type = $instance['settings']['behaviors']['og_widget'][$field_mode]['widget_type']; + $mocked_instance['widget']['type'] = $widget_type; + + // Set the widget's module. + $widget_info = field_info_widget_types($widget_type); + $mocked_instance['widget']['module'] = $widget_info['module']; + $mocked_instance['widget']['settings'] = drupal_array_merge_deep($mocked_instance['widget']['settings'], $widget_info['settings']); + + // See OgSelectionHandler::buildEntityFieldQuery(). + $mocked_instance['field_mode'] = $field_mode; + + if (!empty($mocked_instance['default_value_function']) && $mocked_instance['default_value_function'] == 'entityreference_prepopulate_field_default_value' && user_access('administer group') && module_exists('entityreference_prepopulate')) { + // If the default value function is to Entityreference-prepopualte + // we change it to our own function, that will filter default values + // according to the field-mode. + $mocked_instance['default_value_function'] = 'og_prepopulate_field_default_value'; + } + + return $mocked_instance; +} + +/** + * Rebuild the element's values, using the default and admin if exists. + */ +function og_complex_widget_element_validate($element, &$form_state, $form) { + $subform = drupal_array_get_nested_value($form_state['values'], $element['#array_parents']); + $ids = $element['#other_groups_ids']; + foreach (array('default', 'admin') as $type) { + if (empty($subform[$type])) { + continue; + } + + foreach ($subform[$type] as $delta => $value) { + if (!empty($value['target_id']) && is_numeric($value['target_id'])) { + $ids[] = array('target_id' => $value['target_id']); + } + } + } + + // Set the form values by directly using drupal_array_set_nested_values(), + // which allows us to control the element parents. In this case we cut off the + // last element that contains the delta 0, as $ids is already keyed with + // deltas. + drupal_array_set_nested_value($form_state['values'], array_slice($element['#parents'], 0, -1), $ids, TRUE); +} + +/** + * After build; Remove the "Add more" button. + * + * @see field_multiple_value_form() + * @see theme_field_multiple_value_form() + */ +function og_complex_widget_after_build($form, &$form_state) { + foreach (og_get_group_audience_fields($form['#entity_type'], $form['#bundle']) as $field_name => $value) { + if (empty($form[$field_name])) { + continue; + } + unset($form[$field_name][LANGUAGE_NONE]['#theme']); + unset($form[$field_name][LANGUAGE_NONE]['add_more']); + unset($form[$field_name][LANGUAGE_NONE][0]['_weight']); + + if (!empty($form[$field_name][LANGUAGE_NONE][0]['admin'])) { + // Wrap both elements with a fieldset. + $form[$field_name][LANGUAGE_NONE]['#theme_wrappers'] = array('fieldset'); + } + } + + return $form; +} + +/** + * Menu callback: autocomplete the label of an entity. + * + * @param $type + * The widget type (i.e. 'single' or 'tags'). + * @param $field_name + * The name of the entity-reference field. + * @param $entity_type + * The entity type. + * @param $bundle_name + * The bundle name. + * @param $field_mode + * The field mode, "default" or "admin". + * @param $entity_id + * Optional; The entity ID the entity-reference field is attached to. + * Defaults to ''. + * @param $string + * The label of the entity to query by. + * + * @see entityreference_autocomplete_callback() + */ +function og_entityreference_autocomplete_callback($type, $field_name, $entity_type, $bundle_name, $field_mode, $entity_id = '', $string = '') { + $field = field_info_field($field_name); + $instance = field_info_instance($entity_type, $field_name, $bundle_name); + $instance = og_get_mocked_instance($instance, $field_mode); + $matches = array(); + + if (!$field || !$instance || $field['type'] != 'entityreference' || !field_access('edit', $field, $entity_type)) { + return MENU_ACCESS_DENIED; + } + + $entity = NULL; + if ($entity_id !== 'NULL') { + $entity = entity_load_single($entity_type, $entity_id); + if (!$entity || !entity_access('view', $entity_type, $entity)) { + return MENU_ACCESS_DENIED; + } + } + $handler = entityreference_get_selection_handler($field, $instance, $entity_type, $entity); + + if ($type == 'tags') { + // The user enters a comma-separated list of tags. We only autocomplete the last tag. + $tags_typed = drupal_explode_tags($string); + $tag_last = drupal_strtolower(array_pop($tags_typed)); + if (!empty($tag_last)) { + $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; + } + } + else { + // The user enters a single tag. + $prefix = ''; + $tag_last = $string; + } + + if (!empty($tag_last)) { + // Get an array of matching entities. + $entity_labels = $handler->getReferencableEntities($tag_last, $instance['widget']['settings']['match_operator'], 10); + + // Loop through the products and convert them into autocomplete output. + foreach ($entity_labels as $entity_id => $label) { + $key = "$label ($entity_id)"; + // Strip things like starting/trailing white spaces, line breaks and tags. + $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(decode_entities(strip_tags($key))))); + // Names containing commas or quotes must be wrapped in quotes. + if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) { + $key = '"' . str_replace('"', '""', $key) . '"'; + } + $matches[$prefix . $key] = '
' . $label . '
'; + } + } + + drupal_json_output($matches); +} + +/** + * Default value callback; Filter results by field-mode. + */ +function og_prepopulate_field_default_value($entity_type, $entity, $field, $instance, $langcode) { + if ($items = entityreference_prepopulate_get_values_from_url($field, $instance)) { + // Filter out the items that don't match the field-mode. + $gids = array(); + foreach ($items as $item) { + $gids[] = $item['target_id']; + } + if (!$valid_ids = entityreference_get_selection_handler($field, $instance, $entity_type, $entity)->validateReferencableEntities($gids)) { + return; + } + $valid_items = array(); + foreach ($valid_ids as $valid_id) { + $valid_items[] = array('target_id' => $valid_id); + } + return $valid_items; + } +} \ No newline at end of file diff --git a/og.module b/og.module index 8da9558..abc9763 100644 --- a/og.module +++ b/og.module @@ -5,6 +5,9 @@ * Enable users to create and manage groups with roles and permissions. */ +// Add field widget related code. +require drupal_get_path('module', 'og') . '/includes/og.field.inc'; + /** * Define active group content states. * @@ -639,318 +642,6 @@ function og_views_api() { ); } -/** - * Implements hook_field_widget_info(). - */ -function og_field_widget_info() { - $widgets['og_complex'] = array( - 'label' => t('OG reference'), - 'description' => t('Complex widget to reference groups.'), - 'field types' => array('entityreference'), - ); - - return $widgets; -} - - -/** - * Implements hook_field_widget_form(). - */ -function og_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - $entity_type = $instance['entity_type']; - $entity = isset($element['#entity']) ? $element['#entity'] : NULL; - - if (!$entity) { - return; - } - - if ($field['settings']['handler'] != 'og') { - $params = array('%label' => $instance['label']); - form_error($form, t('Field %label is a group-audience but its Entity selection mode is not defined as "Organic groups" in the field settings page.', $params)); - return; - } - - // Cache the processed entity, to make sure we call the widget only once. - $cache = &drupal_static(__FUNCTION__, array()); - list($id,, $bundle) = entity_extract_ids($entity_type, $entity); - $field_name = $field['field_name']; - - $identifier = $field_name . ':' . $entity_type . ':' . $id; - if (isset($cache[$identifier])) { - return; - } - $cache[$identifier] = TRUE; - - ctools_include('fields'); - - $field_modes = array('default'); - $has_admin = FALSE; - - // The group IDs that might not be accessible by the user, but we need - // to keep even after saving. - $element['#other_groups_ids'] = array(); - $element['#element_validate'][] = 'og_complex_widget_element_validate'; - - if (user_access('administer group')) { - $has_admin = TRUE; - $field_modes[] = 'admin'; - } - - // Get the "Other group" group IDs. - $target_type = $field['settings']['target_type']; - $entity_gids = og_get_entity_groups($entity_type, $entity); - $entity_gids = !empty($entity_gids[$target_type]) ? $entity_gids[$target_type] : array(); - - $user_gids = og_get_entity_groups(); - $user_gids = !empty($user_gids[$target_type]) ? $user_gids[$target_type] : array(); - - $other_groups_ids = array(); - $full_diff = array_diff(array_merge($entity_gids, $user_gids), array_intersect($entity_gids, $user_gids)); - $other_groups_ids = array_values($full_diff); - - foreach ($field_modes as $field_mode) { - $mocked_instance = og_get_mocked_instance($instance, $field_mode); - $dummy_entity = clone $entity; - - if ($has_admin) { - $mocked_instance['required'] = FALSE; - $mocked_instance['label'] = $field_mode == 'default' ? t('Default') : t('Administrator'); - - if ($id) { - // The field might be required, and it will throw an exception - // when we try to set an empty value, so change the wrapper's - // info. - $wrapper = entity_metadata_wrapper($entity_type, $dummy_entity, array('property info alter' => 'og_property_info_alter', 'field name' => $field_name)); - if ($field_mode == 'admin') { - // Keep only the hidden group IDs on the entity, so they won't - // appear again on the "admin" field, for example on an autocomplete - // widget type. - $valid_ids = $other_groups_ids ? entityreference_get_selection_handler($field, $mocked_instance, $entity_type, $dummy_entity)->validateReferencableEntities($other_groups_ids) : array(); - $valid_ids = $field['cardinality'] == 1 ? reset($valid_ids) : $valid_ids; - $wrapper->{$field_name}->set($valid_ids ? $valid_ids : NULL); - } - else { - // Keep only the groups that belong to the user and to the entity. - $my_group_ids = array_values(array_intersect($user_gids, $entity_gids)); - $valid_ids = $my_group_ids ? entityreference_get_selection_handler($field, $mocked_instance, $entity_type, $dummy_entity)->validateReferencableEntities($my_group_ids) : array(); - - $valid_ids = $field['cardinality'] == 1 ? reset($valid_ids) : $valid_ids; - $wrapper->{$field_name}->set($valid_ids ? $valid_ids : NULL); - } - } - } - else { - // Non-admin user. - $mocked_instance_other_groups = $mocked_instance; - $mocked_instance_other_groups['field_mode'] = 'admin'; - if ($other_groups_ids && $valid_ids = entityreference_get_selection_handler($field, $mocked_instance_other_groups, $entity_type, $dummy_entity)->validateReferencableEntities($other_groups_ids)) { - foreach ($valid_ids as $id) { - $element['#other_groups_ids'][] = array('target_id' => $id); - } - } - } - - $dummy_form_state = $form_state; - if (empty($form_state['rebuild'])) { - // Form is "fresh" (i.e. not call from field_add_more_submit()), so - // re-set the items-count, to show the correct amount for the mocked - // instance. - $dummy_form_state['field'][$field_name][LANGUAGE_NONE]['items_count'] = !empty($dummy_entity->{$field_name}[LANGUAGE_NONE]) ? count($dummy_entity->{$field_name}[LANGUAGE_NONE]) : 0; - } - - $new_element = ctools_field_invoke_field($mocked_instance, 'form', $entity_type, $dummy_entity, $form, $dummy_form_state, array('default' => TRUE)); - $element[$field_mode] = $new_element[$field_name][LANGUAGE_NONE]; - - if (in_array($mocked_instance['widget']['type'], array('entityreference_autocomplete', 'entityreference_autocomplete_tags'))) { - // Change the "Add more" button name so it adds only the needed - // element. - if (!empty($element[$field_mode]['add_more']['#name'])) { - $element[$field_mode]['add_more']['#name'] .= '__' . $field_mode; - } - - foreach (array_keys($element[$field_mode]) as $delta) { - if (!is_numeric($delta)) { - continue; - } - - $sub_element = &$element[$field_mode][$delta]['target_id']; - - // Rebuild the autocomplete path. - $path = explode('/', $sub_element['#autocomplete_path']); - $sub_element['#autocomplete_path'] = 'og/autocomplete'; - - // Add autocomplete type - $sub_element['#autocomplete_path'] .= "/$path[2]/$path[3]/$path[4]/$path[5]"; - - // Add field mode. - $sub_element['#autocomplete_path'] .= "/$field_mode"; - - // Add the entity ID. - $sub_element['#autocomplete_path'] .= "/$path[6]"; - if (!empty($path[7])) { - // Add the text. - $sub_element['#autocomplete_path'] .= "/$path[7]"; - } - } - } - } - - $form['#after_build']['og'] = 'og_complex_widget_after_build'; - - return $element; -} - -/** - * Property info alter; Change mocked field to be non-required. - */ -function og_property_info_alter($wrapper, $info) { - $property_info = $wrapper->info(); - $field_name = $property_info['field name']; - $info['properties'][$field_name]['required'] = FALSE; - return $info; -} - -/** - * Helper function; Get the mocked instance. - */ -function og_get_mocked_instance($instance, $field_mode) { - $mocked_instance = $instance; - $widget_type = $instance['settings']['behaviors']['og_widget'][$field_mode]['widget_type']; - $mocked_instance['widget']['type'] = $widget_type; - - // Set the widget's module. - $widget_info = field_info_widget_types($widget_type); - $mocked_instance['widget']['module'] = $widget_info['module']; - $mocked_instance['widget']['settings'] = drupal_array_merge_deep($mocked_instance['widget']['settings'], $widget_info['settings']); - - // See OgSelectionHandler::buildEntityFieldQuery(). - $mocked_instance['field_mode'] = $field_mode; - - return $mocked_instance; -} - -/** - * Rebuild the element's values, using the default and admin if exists. - */ -function og_complex_widget_element_validate($element, &$form_state, $form) { - $subform = drupal_array_get_nested_value($form_state['values'], $element['#array_parents']); - $ids = $element['#other_groups_ids']; - foreach (array('default', 'admin') as $type) { - if (empty($subform[$type])) { - continue; - } - - foreach ($subform[$type] as $delta => $value) { - if (!empty($value['target_id']) && is_numeric($value['target_id'])) { - $ids[] = array('target_id' => $value['target_id']); - } - } - } - - // Set the form values by directly using drupal_array_set_nested_values(), - // which allows us to control the element parents. In this case we cut off the - // last element that contains the delta 0, as $ids is already keyed with - // deltas. - drupal_array_set_nested_value($form_state['values'], array_slice($element['#parents'], 0, -1), $ids, TRUE); -} - -/** - * After build; Remove the "Add more" button. - * - * @see field_multiple_value_form() - * @see theme_field_multiple_value_form() - */ -function og_complex_widget_after_build($form, &$form_state) { - foreach (og_get_group_audience_fields($form['#entity_type'], $form['#bundle']) as $field_name => $value) { - if (empty($form[$field_name])) { - continue; - } - unset($form[$field_name][LANGUAGE_NONE]['#theme']); - unset($form[$field_name][LANGUAGE_NONE]['add_more']); - unset($form[$field_name][LANGUAGE_NONE][0]['_weight']); - - if (!empty($form[$field_name][LANGUAGE_NONE][0]['admin'])) { - // Wrap both elements with a fieldset. - $form[$field_name][LANGUAGE_NONE]['#theme_wrappers'] = array('fieldset'); - } - } - - return $form; -} - -/** - * Menu callback: autocomplete the label of an entity. - * - * @param $type - * The widget type (i.e. 'single' or 'tags'). - * @param $field_name - * The name of the entity-reference field. - * @param $entity_type - * The entity type. - * @param $bundle_name - * The bundle name. - * @param $field_mode - * The field mode, "default" or "admin". - * @param $entity_id - * Optional; The entity ID the entity-reference field is attached to. - * Defaults to ''. - * @param $string - * The label of the entity to query by. - * - * @see entityreference_autocomplete_callback() - */ -function og_entityreference_autocomplete_callback($type, $field_name, $entity_type, $bundle_name, $field_mode, $entity_id = '', $string = '') { - $field = field_info_field($field_name); - $instance = field_info_instance($entity_type, $field_name, $bundle_name); - $instance = og_get_mocked_instance($instance, $field_mode); - $matches = array(); - - if (!$field || !$instance || $field['type'] != 'entityreference' || !field_access('edit', $field, $entity_type)) { - return MENU_ACCESS_DENIED; - } - - $entity = NULL; - if ($entity_id !== 'NULL') { - $entity = entity_load_single($entity_type, $entity_id); - if (!$entity || !entity_access('view', $entity_type, $entity)) { - return MENU_ACCESS_DENIED; - } - } - $handler = entityreference_get_selection_handler($field, $instance, $entity_type, $entity); - - if ($type == 'tags') { - // The user enters a comma-separated list of tags. We only autocomplete the last tag. - $tags_typed = drupal_explode_tags($string); - $tag_last = drupal_strtolower(array_pop($tags_typed)); - if (!empty($tag_last)) { - $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; - } - } - else { - // The user enters a single tag. - $prefix = ''; - $tag_last = $string; - } - - if (!empty($tag_last)) { - // Get an array of matching entities. - $entity_labels = $handler->getReferencableEntities($tag_last, $instance['widget']['settings']['match_operator'], 10); - - // Loop through the products and convert them into autocomplete output. - foreach ($entity_labels as $entity_id => $label) { - $key = "$label ($entity_id)"; - // Strip things like starting/trailing white spaces, line breaks and tags. - $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(decode_entities(strip_tags($key))))); - // Names containing commas or quotes must be wrapped in quotes. - if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) { - $key = '"' . str_replace('"', '""', $key) . '"'; - } - $matches[$prefix . $key] = '
' . $label . '
'; - } - } - - drupal_json_output($matches); -} /** * Implements hook_field_create_instance().