=== modified file 'includes/install.inc' --- includes/install.inc 2009-01-18 07:00:09 +0000 +++ includes/install.inc 2009-01-30 23:17:27 +0000 @@ -490,8 +490,10 @@ function drupal_verify_profile($profile, * * @param $module_list * The modules to install. + * @param $disable_modules_installed_hook + * Normally just testing wants to set this to TRUE. */ -function drupal_install_modules($module_list = array()) { +function drupal_install_modules($module_list = array(), $disable_modules_installed_hook = FALSE) { $files = module_rebuild_cache(); $module_list = array_flip(array_values($module_list)); do { @@ -511,7 +513,7 @@ function drupal_install_modules($module_ asort($module_list); $module_list = array_keys($module_list); $modules_installed = array_filter($module_list, '_drupal_install_module'); - if (!empty($modules_installed)) { + if (!$disable_modules_installed_hook && !empty($modules_installed)) { module_invoke_all('modules_installed', $modules_installed); } module_enable($module_list); === modified file 'includes/theme.inc' --- includes/theme.inc 2009-01-27 01:00:13 +0000 +++ includes/theme.inc 2009-01-30 23:17:27 +0000 @@ -624,7 +624,9 @@ function theme() { } if (isset($info['function'])) { // The theme call is a function. - $output = call_user_func_array($info['function'], $args); + if (drupal_function_exists($info['function'])) { + $output = call_user_func_array($info['function'], $args); + } } else { // The theme call is a template. @@ -2004,7 +2006,11 @@ function template_preprocess_node(&$vari } // Clean up name so there are no underscores. $variables['template_files'][] = 'node-' . str_replace('_', '-', $node->type); - $variables['template_files'][] = 'node-' . $node->nid; + $variables['template_files'][] = 'node-' . $node->nid; + + // Add $FIELD_NAME_rendered variables for fields. + drupal_function_exists('field_attach_preprocess'); + $variables += field_attach_preprocess('node', $node); } /** === added directory 'modules/field' === added file 'modules/field/api.field.php' --- modules/field/api.field.php 1970-01-01 00:00:00 +0000 +++ modules/field/api.field.php 2009-01-30 23:17:27 +0000 @@ -0,0 +1,765 @@ + array( + 'name' => t('Node'), + 'id key' => 'nid', + 'revision key' => 'vid', + 'bundle key' => 'type', + // Node.module handles its own caching. + 'cacheable' => FALSE, + // Bundles must provide human readable name so + // we can create help and error messages about them. + 'bundles' => node_get_types('names'), + ), + ); + return $return; +} + +/** + * @} End of "ingroup field_fieldable_type" + */ + +/** + * @defgroup field_types Field Types API + * @{ + * Define field types, widget types, and display formatter types. + * + * The bulk of the Field Types API are related to field types. A + * field type represents a particular data storage type (integer, + * string, date, etc.) that can be attached to a fieldable object. + * hook_field_info() defines the basic properties of a field type, and + * a variety of other field hooks are called by the Field Attach API + * to perform field-type-specific actions. + * + * The Field Types API also defines widget types via + * hook_field_widget_info(). Widgets are Form API elements with + * additional processing capabilities. A field module can define + * widgets that work with its own field types or with any other + * module's field types. Widget hooks are typically called by the + * Field Attach API when creating the field form elements during + * field_attach_form(). + * + * TODO Display formatters. + */ + +/** + * Define Field API field types. + * + * @return + * An array whose keys are field type names and whose values are: + * + * label: TODO + * description: TODO + * settings: TODO + * instance_settings: TODO + * default_widget: TODO + * default_formatter: TODO + * behaviors: TODO + */ +function hook_field_info() { + return array( + 'text' => array( + 'label' => t('Text'), + 'description' => t('This field stores varchar text in the database.'), + 'settings' => array('max_length' => 255), + 'instance_settings' => array('text_processing' => 0), + 'default_widget' => 'text_textfield', + 'default_formatter' => 'text_default', + ), + 'textarea' => array( + 'label' => t('Textarea'), + 'description' => t('This field stores long text in the database.'), + 'instance_settings' => array('text_processing' => 0), + 'default_widget' => 'text_textarea', + 'default_formatter' => 'text_default', + ), + ); +} + +/** + * Define the Field API schema for a field structure. + * + * @param $field + * A field structure. + * @return + * A Field API schema is an array of Schema API column + * specifications, keyed by field-independent column name. For + * example, a field may declare a column named 'value'. The SQL + * storage engine may create a table with a column named + * _value_0, but the Field API schema column name is + * still 'value'. + */ +function hook_field_columns($field) { + if ($field['type'] == 'textarea') { + $columns = array( + 'value' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + ); + } + else { + $columns = array( + 'value' => array( + 'type' => 'varchar', + 'length' => $field['settings']['max_length'], + 'not null' => FALSE, + ), + ); + } + $columns += array( + 'format' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + return $columns; +} + +/** + * Define Field API widget types. + * + * @return + * An array whose keys are field type names and whose values are: + * + * label: TODO + * description: TODO + * field types: TODO + * settings: TODO + * behaviors: TODO + */ +function hook_field_widget_info() { +} + +/* + * Define Field API formatter types. + * + * @return + * An array whose keys are field type names and whose values are: + * + * label: TODO + * description: TODO + * field types: TODO + * behaviors: TODO + */ +function hook_field_formatter_info() { +} + +/** + * Define custom load behavior for this module's field types. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_load($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom validate behavior for this module's field types. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + * @param $form + * The form structure being validated. NOTE: This parameter will + * become obsolete (see field_attach_validate()). + */ +function hook_field_validate($obj_type, $object, $field, $instance, $items, $form) { +} + +/** + * Define custom presave behavior for this module's field types. + * TODO: The behavior of this hook is going to change (see + * field_attach_presave()). + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_presave($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom insert behavior for this module's field types. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_insert($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom update behavior for this module's field types. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_update($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom delete behavior for this module's field types. This + * hook is invoked just before the data is deleted from field storage. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_delete($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom delete_revision behavior for this module's field + * types. This hook is invoked just before the data is deleted from + * field storage, and will only be called for fieldable types that are + * versioned. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_delete_revision($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom sanitize behavior for this module's field types. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_sanitize($obj_type, $object, $field, $instance, $items) { +} + +/** + * Define custom prepare_translation behavior for this module's field + * types. TODO: This hook may or may not survive in Field API. + * + * @param $obj_type + * The type of $object. + * @param $object + * The object for the operation. + * @param $field + * The field structure for the operation. + * @param $instance + * The instance structure for $field on $object's bundle. + * @param $items + * $object->{$field['field_name']}, or an empty array if unset. + */ +function hook_field_prepare_translation($obj_type, $object, $field, $instance, $items) { +} + +/** + * Return a single form element for a form. + * + * It will be built out and validated in the callback(s) listed in + * hook_elements. We build it out in the callbacks rather than in + * hook_field_widget so it can be plugged into any module that can + * provide it with valid $field information. + * + * Field API will set the weight, field name and delta values for each + * form element. If there are multiple values for this field, the + * Field API will call this function as many times as needed. + * + * @param $form + * The entire form array, $form['#node'] holds node information. + * TODO: Not #node any more. + * @param $form_state + * The form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * The field structure. + * @param $instance + * The field instance. + * @param $items + * Array of default values for this field. + * @param $delta + * The order of this item in the array of subelements (0, 1, 2, etc). + * @return + * The form item for a single element for this field. + */ +function hook_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = 0) { + $element = array( + '#type' => $instance['widget']['type'], + '#default_value' => isset($items[$delta]) ? $items[$delta] : '', + ); + return $element; +} + +/** + * @} End of "ingroup field_type" + */ + +/** + * @ingroup field_attach + * @{ + */ + +/** + * Act on field_attach_form. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_form() for details and arguments. + */ +function hook_field_attach_form($obj_type, $object, &$form, &$form_state) { +} + +/** + * Act on field_attach_load. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_load() for details and arguments. TODO: + * Currently, this hook only accepts a single object a time. + */ +function hook_field_attach_load($obj_type, $object) { +} + +/** + * Act on field_attach_validate. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_validate() for details and arguments. + */ +function hook_field_attach_validate($obj_type, $object, &$form) { +} + +/** + * Act on field_attach_submit. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_submit() for details and arguments. + */ +function hook_field_attach_submit($obj_type, $object, $form, &$form_state) { +} + +/** + * Act on field_attach_presave. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_presave() for details and arguments. + */ +function hook_field_attach_presave($obj_type, $object) { +} + +/** + * Act on field_attach_insert. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_insert() for details and arguments. + */ +function hook_field_attach_insert($obj_type, $object) { +} + +/** + * Act on field_attach_update. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_update() for details and arguments. + */ +function hook_field_attach_update($obj_type, $object) { +} + +/** + * Act on field_attach_delete. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_delete() for details and arguments. + */ +function hook_field_attach_delete($obj_type, $object) { +} + +/** + * Act on field_attach_delete_revision. This hook is invoked after + * the field module has performed the operation. + * + * See field_attach_delete_revision() for details and arguments. + */ +function hook_field_attach_delete_revision($obj_type, $object) { +} + +/** + * Act on field_attach_view. This hook is invoked after the field module + * has performed the operation. + * + * @param $output + * The structured content array tree for all of $object's fields. + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $teaser + * Whether to display the teaser only, as on the main page. + */ +function hook_field_attach_view($output, $obj_type, $object, $teaser) { +} + +/** + * Act on field_attach_create_bundle. This hook is invoked after the + * field module has performed the operation. + * + * See field_attach_create_bundle() for details and arguments. + */ +function hook_field_attach_create_bundle($bundle) { +} + +/** + * Act on field_attach_rename_bundle. This hook is invoked after the + * field module has performed the operation. + * + * See field_attach_rename_bundle() for details and arguments. + */ +function hook_field_rename_bundle($bundle_old, $bundle_new) { +} + +/** + * Act on field_attach_delete_bundle. This hook is invoked after the field module + * has performed the operation. + * + * See field_attach_delete_bundle() for details and arguments. + */ +function hook_field_attach_delete_bundle($bundle) { +} + +/** + * @} End of "ingroup field_attach" + */ + +/********************************************************************** + * Field Storage API + **********************************************************************/ + +/** + * @ingroup field_storage + * @{ + */ + +/** + * Load field data for a set of objects. + * + * @param $obj_type + * The entity type of objects being loaded, such as 'node' or + * 'user'. + * @param $objects + * The array of objects for which to load data. + * @param $age + * FIELD_LOAD_CURRENT to load the most recent revision for all + * fields, or FIELD_LOAD_REVISION to load the version indicated by + * each object. + * @return + * An array of field data for the objects, keyed by entity id, field + * name, and item delta number. + */ +function hook_field_storage_load($obj_type, $queried_objs, $age) { +} + +/** + * Write field data for an object. + * + * @param $obj_type + * The entity type of object, such as 'node' or 'user'. + * @param $object + * The object on which to operate. + * @param $update + * TRUE if this is an update to an existing object, FALSE if it is + * an insert of a new object. + */ +function hook_field_storage_write($obj_type, $object, TRUE) { +} + +/** + * Delete all field data for an object. + * + * @param $obj_type + * The entity type of object, such as 'node' or 'user'. + * @param $object + * The object on which to operate. + */ +function hook_field_storage_delete($obj_type, $object) { +} + +/** + * Delete a single revision of field data for an object. + * + * @param $obj_type + * The entity type of object, such as 'node' or 'user'. + * @param $object + * The object on which to operate. The revision to delete is + * indicated by the object's revision id property, as identified by + * hook_fieldable_info() for $obj_type. + */ +function hook_field_storage_delete_revision($obj_type, $object) { +} + +/** + * Act on creation of a new bundle. + * + * @param $bundle + * The name of the bundle being created. + */ +function hook_field_storage_create_bundle($bundle) { +} + +/** + * Act on a bundle being renamed. + * + * @param $bundle_old + * The old name of the bundle. + * @param $bundle_new + * The new name of the bundle. + */ +function hook_field_storage_rename_bundle($bundle_old, $bundle_new) { +} + +/** + * Act on creation of a new field. + * + * @param $field + * The field structure being created. + */ +function hook_field_storage_create_field($field) { +} + +/** + * Act on deletion of a field. + * + * @param $field_name + * The name of the field being deleted. + */ +function hook_field_storage_delete_field($field_name) { +} + +/** + * Act on deletion of a field instance. + * + * @param $field_name + * The name of the field in the new instance. + * @param $bundle + * The name of the bundle in the new instance. + */ +function hook_field_storage_delete_instance($field_name, $bundle) { +} + +/** + * @} End of "ingroup field_storage" + */ + +/********************************************************************** + * Field CRUD API + **********************************************************************/ + +/** + * @ingroup field_crud + * @{ + */ + +/** + * Act on a field being created. This hook is invoked after the field + * is created and so it cannot modify the field itself. + * + * TODO: Not implemented. + * + * @param $field + * The field just created. + */ +function hook_field_create_field($field) { +} + +/** + * Act on a field instance being created. This hook is invoked after + * the instance record is saved and so it cannot modify the instance + * itself. + * + * @param $instance + * The instance just created. + */ +function hook_field_create_instance($instance) { +} + +/** + * Act on a field being deleted. This hook is invoked just before the + * field is deleted. + * + * TODO: Not implemented. + * + * @param $field + * The field being deleted. + */ +function hook_field_delete_field($field) { +} + + +/** + * Act on a field instance being updated. This hook is invoked after + * the instance record is saved and so it cannot modify the instance + * itself. + * + * TODO: Not implemented. + * + * @param $instance + * The instance just updated. + */ +function hook_field_update_instance($instance) { +} + +/** + * Act on a field instance being deleted. This hook is invoked just + * before the instance is deleted. + * + * TODO: Not implemented. + * + * @param $instance + * The instance just updated. + */ +function hook_field_delete_instance($instance) { +} + +/** + * Act on field records being read from the database. + * + * @param $field + * The field record just read from the database. + */ +function hook_field_read_field($field) { +} + +/** + * Act on a field record being read from the database. + * + * @param $instance + * The instance record just read from the database. + */ +function hook_field_read_instance($instance) { +} + +/** + * @} End of "ingroup field_crud" + */ + +/********************************************************************** + * TODO: I'm not sure where these belong yet. + **********************************************************************/ + +/** + * TODO + * + * Note : Right now this belongs to the "Fieldable Type API". + * Whether 'build modes' is actually a 'fields' concept is to be debated + * in a separate overhaul patch for core. + */ +function hook_field_build_modes($obj_type) { +} + +/** + * Determine whether the user has access to a given field. + * + * @param $op + * The operation to be performed. Possible values: + * - "edit" + * - "view" + * @param $field + * The field on which the operation is to be performed. + * @param $account + * (optional) The account to check, if not given use currently logged in user. + * @return + * TRUE if the operation is allowed; + * FALSE if the operation is denied. + */ +function hook_field_access($op, $field, $account) { +} === added file 'modules/field/field.attach.inc' --- modules/field/field.attach.inc 1970-01-01 00:00:00 +0000 +++ modules/field/field.attach.inc 2009-01-30 23:17:27 +0000 @@ -0,0 +1,619 @@ +$field_name) ? $object->$field_name : array(); + + // Make sure AHAH 'add more' button isn't sent to the fields for processing. + // TODO D7 : needed ? + unset($items[$field_name .'_add_more']); + + $function = $default ? 'field_default_'. $op : $field['module'] .'_field_'. $op; + if (drupal_function_exists($function)) { + $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); + if (is_array($result)) { + $return = array_merge($return, $result); + } + else if (isset($result)) { + $return[] = $result; + } + } + // Put back the altered items in the object, if the field was present to + // begin with (avoid replacing missing field with empty array(), those are + // not semantically equivalent on update). + if (isset($object->$field_name)) { + $object->$field_name = $items; + } + } + + return $return; +} + +/** + * Invoke field.module's version of a field hook. + */ +function _field_invoke_default($op, $obj_type, &$object, &$a = NULL, &$b = NULL) { + return _field_invoke($op, $obj_type, $object, $a, $b, TRUE); +} + +/** + * @} End of "defgroup field_attach" + * + * The rest of the functions in this file are not in a group, but + * their automatically-generated autoloaders are (see field.autoload.inc). + */ + +/** + * Add form elements for all fields for an object to a form structure. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object for which to load form elements, used to initialize + * default form values. + * @param $form + * The form structure to fill in. + * @param $form_state + * An associative array containing the current state of the form. + * + * TODO : document the resulting $form structure, like we do for + * field_attach_view(). + */ +function _field_attach_form($obj_type, $object, &$form, $form_state) { + // TODO : something's not right here : do we alter the form or return a value ? + $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state); + + // Let other modules make changes to the form. + foreach (module_implements('field_attach_form') as $module) { + $function = $module .'_field_attach_form'; + $function($obj_type, $object, $form, $form_state); + } +} + +/** + * Load all fields for the most current version of each of a set of + * objects of a single object type. + * + * @param $obj_type + * The type of objects for which to load fields; e.g. 'node' or + * 'user'. + * @param $objects + * An array of objects for which to load fields. The keys for + * primary id and bundle name to load are identified by + * hook_fieldable_info for $obj_type. + * @param $age + * FIELD_LOAD_CURRENT to load the most recent revision for all + * fields, or FIELD_LOAD_REVISION to load the version indicated by + * each object. Defaults to FIELD_LOAD_CURRENT; use + * field_attach_load_revision() instead of passing FIELD_LOAD_REVISION. + * @returns + * On return, the objects in $objects are modified by having the + * appropriate set of fields added. + */ +function _field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT) { + $queried_objs = array(); + + // Fetch avaliable nodes from cache. + foreach ($objects as $obj) { + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $obj); + $cid = "field:$obj_type:$id:$vid"; + if ($cacheable && $cached = cache_get($cid, 'cache_field')) { + foreach ($cached->data as $key => $value) { + $obj->$key = $value; + } + } + else { + $queried_objs[$id] = $objects[$id]; + } + } + // Fetch other nodes from the database. + if ($queried_objs) { + // We need the raw additions to be able to cache them, so + // content_storage_load() and hook_field_load() must not alter + // nodes directly but return their additions. + $additions = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objs, $age); + foreach ($additions as $id => $obj_additions) { + foreach ($obj_additions as $key => $value) { + $queried_objs[$id]->$key = $value; + } + } + + // TODO D7 : to be consistent we might want to make hook_field_load() accept + // multiple objects too. Which forbids going through _field_invoke(), but + // requires manually iterating the instances instead. + foreach ($queried_objs as $id => $object) { + $custom_additions = _field_invoke('load', $obj_type, $object); + foreach ($custom_additions as $key => $value) { + $queried_objs[$id]->$key = $value; + $additions[$id][$key] = $value; + } + + // Let other modules act on loading the object. + foreach (module_implements('field_attach_load') as $module) { + $function = $module .'_field_attach_load'; + $function($obj_type, $queried_objs[$id]); + } + + // Cache the data. + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $obj); + if ($cacheable) { + $cid = "field:$obj_type:$id:$vid"; + $data = isset($additions[$id]) ? $additions[$id] : array(); + cache_set($cid, $data, 'cache_field'); + } + } + } +} + +/** + * Load all fields for a previous version of each of a set of + * objects of a single object type. + * + * @param $obj_type + * The type of objects for which to load fields; e.g. 'node' or + * 'user'. + * @param $objects + * An array of objects for which to load fields. The keys for + * primary id, revision id, and bundle name to load are identified by + * hook_fieldable_info for $obj_type. + * @returns + * On return, the objects in $objects are modified by having the + * appropriate set of fields added. + */ +function _field_attach_load_revision($obj_type, $objects) { + return field_attach_load($obj_type, $objects, FIELD_LOAD_REVISION); +} + +/** + * Perform field validation against the field data in an object. + * Field validation is distinct from widget validation; the latter + * occurs during the Form API validation phase. + * + * NOTE: This functionality does not yet exist in its final state. + * Eventually, field validation will occur during field_attach_insert + * or _update which will throw an exception on failure. For now, + * fieldable entities must call this during their Form API validation + * phase, and field validation will call form_set_error for any + * errors. See http://groups.drupal.org/node/18019. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to validate. + */ +function _field_attach_validate($obj_type, &$object, $form = NULL) { + _field_invoke('validate', $obj_type, $object, $form); + _field_invoke_default('validate', $obj_type, $object, $form); + + // Let other modules validate the object. + foreach (module_implements('field_attach_validate') as $module) { + $function = $module .'_field_attach_validate'; + $function($obj_type, $object, $form); + } +} + +/** + * Perform necessary operations on field data submitted by a form. + * + * Currently, this accounts for drag-and-drop reordering of + * field values, and filtering of empty values. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object being submitted. The 'bundle key', 'id key' and (if applicable) + * 'revision key' should be present. The actual field values will be read + * from $form_state['values']. + * @param $form + * The form structure to fill in. + * @param $form_state + * An associative array containing the current state of the form. + */ +function _field_attach_submit($obj_type, &$object, $form, &$form_state) { + _field_invoke_default('submit', $obj_type, $object, $form, $form_state); + + // Let other modules act on submitting the object. + foreach (module_implements('field_attach_submit') as $module) { + $function = $module .'_field_attach_submit'; + $function($obj_type, $object, $form, $form_state); + } +} + +/** + * Perform necessary operations just before fields data get saved. + * + * We take no specific action here, we just give other + * modules the opportunity to act. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to process. + */ +function _field_attach_presave($obj_type, &$object) { + // TODO : to my knowledge, no field module has any use for 'presave' on D6. + // should we keep this ? + _field_invoke('presave', $obj_type, $object); + + // Let other modules act on presaving the object. + foreach (module_implements('field_attach_presave') as $module) { + $function = $module .'_field_attach_presave'; + $function($obj_type, $object); + } +} + +/** + * Save field data for a new object. The passed in object must + * already contain its id and (if applicable) revision id attributes. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to save. + */ +function _field_attach_insert($obj_type, &$object) { + + // Let other modules act on inserting the object. + foreach (module_implements('field_attach_insert') as $module) { + $function = $module .'_field_attach_insert'; + $function($obj_type, $object); + } + + _field_invoke('insert', $obj_type, $object); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object); + + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); + if ($cacheable) { + cache_clear_all("field:$obj_type:$id:", 'cache_field', TRUE); + } +} + +/** + * Save field data for an existing object. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to save. + */ +function _field_attach_update($obj_type, &$object) { + + // Let other modules act on updating the object. + foreach (module_implements('field_attach_update') as $module) { + $function = $module .'_field_attach_update'; + $function($output, $obj_type, $object); + } + + _field_invoke('update', $obj_type, $object); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, TRUE); + + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); + if ($cacheable) { + cache_clear_all("field:$obj_type:$id:$vid", 'cache_field'); + } +} + +/** + * Delete field data for an existing object. This deletes all + * revisions of field data for the object. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object whose field data to delete. + */ +function _field_attach_delete($obj_type, &$object) { + _field_invoke('delete', $obj_type, $object); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete', $obj_type, $object); + + // Let other modules act on deleting the object. + foreach (module_implements('field_attach_delete') as $module) { + $function = $module .'_field_attach_delete'; + $function($obj_type, $object); + } + + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); + if ($cacheable) { + cache_clear_all("field:$obj_type:$id:", 'cache_field', TRUE); + } +} + +/** + * Delete field data for a single revision of an existing object. The + * passed object must have a revision id attribute. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to save. + */ +function _field_attach_delete_revision($obj_type, &$object) { + _field_invoke('delete revision', $obj_type, $object); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_revision', $obj_type, $object); + + // Let other modules act on deleting the revision. + foreach (module_implements('field_attach_delete_revision') as $module) { + $function = $module .'_field_attach_delete_revision'; + $function($obj_type, $object); + } + + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); + if ($cacheable) { + cache_clear_all("field:$obj_type:$id:$vid", 'cache_field'); + } +} + +/** + * Generate and return a structured content array tree suitable for + * drupal_render() for all of the fields on an object. The format of + * each field's rendered content depends on the display formatter and + * its settings. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $teaser + * Whether to display the teaser only, as on the main page. + * @return + * A structured content array tree for drupal_render(). + */ +function _field_attach_view($obj_type, &$object, $teaser = FALSE) { + // Let field modules sanitize their data for output. + _field_invoke('sanitize', $obj_type, $object); + + $output = _field_invoke_default('view', $obj_type, $object, $teaser); + + // Let other modules make changes after rendering the view. + foreach (module_implements('field_attach_view') as $module) { + $function = $module .'_field_attach_view'; + $function($output, $obj_type, $object, $teaser); + } + + return $output; + +} + +/** + * To be called in entity preprocessor. + * + * - Adds $FIELD_NAME_rendered variables + * containing the themed output for the whole field. + * - Adds the formatted values in the 'view' key of the items. + */ +function _field_attach_preprocess($obj_type, &$object) { + return _field_invoke_default('preprocess', $obj_type, $object); +} + +/** + * Implementation of hook_nodeapi_prepare_translation. + * + * TODO D7: We do not yet know if this really belongs in Field API. + */ +function _field_attach_prepare_translation(&$node) { + // Prevent against invalid 'nodes' built by broken 3rd party code. + if (isset($node->type)) { + $type = content_types($node->type); + // Save cycles if the type has no fields. + if (!empty($type['instances'])) { + $default_additions = _field_invoke_default('prepare translation', $node); + $additions = _field_invoke('prepare translation', $node); + // Merge module additions after the default ones to enable overriding + // of field values. + $node = (object) array_merge((array) $node, $default_additions, $additions); + } + } +} + +/** + * Notify field.module that a new bundle was created. + * + * The default SQL-based storage doesn't need to do anytrhing about it, but + * others might. + * + * @param $bundle + * The name of the newly created bundle. + */ +function _field_attach_create_bundle($bundle) { + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_bundle', $bundle); + + // Clear the cache. + field_cache_clear(); + + foreach (module_implements('field_attach_create_bundle') as $module) { + $function = $module .'_field_attach_create_bundle'; + $function($bundle); + } +} + +/** + * Notify field.module that a bundle was renamed. + * + * @param $bundle_old + * The previous name of the bundle. + * @param $bundle_new + * The new name of the bundle. + */ +function _field_attach_rename_bundle($bundle_old, $bundle_new) { + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_rename_bundle', $bundle_old, $bundle_new); + db_update('field_config_instance') + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle_old) + ->execute(); + + // Clear the cache. + field_cache_clear(); + + foreach (module_implements('field_attach_rename_bundle') as $module) { + $function = $module .'_field_attach_rename_bundle'; + $function($bundle_old, $bundle_new); + } +} + +/** + * Notify field.module the a bundle was deleted. + * + * This deletes the data for the field instances as well as the field instances + * themselves. This function actually just marks the data and field instances + * and deleted, leaving the garbage collection for a separate process, because + * it is not always possible to delete this much data in a single page request + * (particularly since for some field types, the deletion is more than just a + * simple DELETE query). + * + * @param $bundle + * The bundle to delete. + */ +function _field_attach_delete_bundle($bundle) { + // Let other modules act on deleting the bundle + foreach (module_implements('field_attach_delete_bundle') as $module) { + $function = $module .'_field_attach_delete_bundle'; + $function($bundle); + } + + // Delete the instances themseves + $instances = field_info_instances($bundle); + foreach ($instances as $instance) { + field_delete_instance($instance['field_name'], $bundle); + } +} + +/** + * Helper function to extract id, vid, and bundle name from an object. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object from which to extract values. + * @return + * A numerically indexed array (not a hash table) containing these + * elements: + * + * 0: primary id of the object + * 1: revision id of the object, or NULL if $obj_type is not versioned + * 2: bundle name of the object + * 3: whether $obj_type's fields should be cached (TRUE/FALSE) + */ +function _field_attach_extract_ids($object_type, $object) { + // TODO D7 : prevent against broken 3rd party $node without 'type'. + $info = field_info_fieldable_types($object_type); + // Objects being created might not have id/vid yet. + $id = isset($object->{$info['id key']}) ? $object->{$info['id key']} : NULL; + $vid = ($info['revision key'] && isset($object->{$info['revision key']})) ? $object->{$info['revision key']} : NULL; + // If no bundle key provided, then we assume a single bundle, named after the + // type of the object. + $bundle = $info['bundle key'] ? $object->{$info['bundle key']} : $object_type; + $cacheable = isset($info['cacheable']) ? $info['cacheable'] : FALSE; + return array($id, $vid, $bundle, $cacheable); +} + +/** + * @autoload} End of "@autoload field_attach" + */ === added file 'modules/field/field.autoload.inc' --- modules/field/field.autoload.inc 1970-01-01 00:00:00 +0000 +++ modules/field/field.autoload.inc 2009-01-30 23:17:27 +0000 @@ -0,0 +1,618 @@ +$field_name. + * - type. The type of the field, such as 'text' or 'image'. Field types + * are defined by modules that implement hook_field_into(). + * - cardinality. The number of values the field can hold. Legal + * values are any positive integer or FIELD_CARDINALITY_UNLIMITED. + * - locked. TODO: undefined. + * - settings. An array of key/value pairs of field-type-specific + * settings. Each field type module defines and documents its own + * field settings. + * - module (read-only). The name of the module that implements the + * field type. + * - active (read-only). TRUE if the module that implements the field + * type is currently enabled, FALSE otherwise. + * - deleted (read-only). TRUE if this field has been deleted, FALSE + * otherwise. Deleted fields are ignored by the Field Attach API. + * This property exists because fields can be marked for deletion but + * only actually destroyed by a separate garbage-collection process. + * - columns (read-only). An array of the Field API columns used to + * store each value of this field. The column list may depend on + * field settings; it is not constant per field type. Field API + * column specifications are exactly like Schema API column + * specifications but, depending on the field storage module in use, + * the name of the column may not represent an actual column in an SQL + * database. + * + * Field Instance objects are (currently) represented as an array of + * key/value pairs. The object properties are: + * + * $instance: + * - field_name. The name of field attached by this instance. + * - bundle. The name of the bundle that the field is attached to. + * - label. A human-readable label for the field when used with this + * bundle. For example, the label will be the title of Form API + * elements for this instance. + * - description. A human-readable description for the field when + * used with this bundle. For example, the description will be the + * help text of Form API elements for this instance. + * - weight. The order in which the field should be sorted relative + * to other fields when used with this bundle. The weight affects + * ordering in both forms (see field_attach_form()) and rendered output + * (see field_attach_view()). + * - required. TRUE if a value for this field is required when used + * with this bundle, FALSE otherwise. Currently, required-ness is + * only enforced during Form API operations, not by + * field_attach_load(), field_attach_insert(), or field_attach_update(). + * - default_value_function. The name of the function, if any, that + * will provide a default value. + * - settings. An array of key/value pairs of field-type-specific + * instance settings. Each field type module defines and documents + * its own instance settings. + * - widget: An array of key/value pairs identifying the Form API + * input widget for the field when used by this bundle. + * - type. The type of the widget, such as textarea or + * option_widgets_select. Widget types are defined by modules that + * implement hook_field_widget_info(). + * - settings. An array of key/value pairs of widget-type-specific + * settings. Each field widget type module defines and documents + * its own widget settings. + * - module (read-only). The name of the module that implements the + * widget type. + * - active (read-only). TRUE if the module that implements the widget + * type is currently enabled, FALSE otherwise. + * - display. TODO. + * - (context_1) + * - label + * - exclude + * - type + * - settings + * - ... + * - module (internal) + * - (context 2) + * - ... + * - deleted (read-only). TRUE if this instance has been deleted, FALSE + * otherwise. Deleted instances are ignored by the Field Attach API. + * This property exists because instances can be marked for deletion but + * only actually destroyed by a separate garbage-collection process. + * + * A Bundle is just a string whose value is the name of the bundle. + * + * TODO D7 : document max length for field types, widget types, + * formatter names... + */ +/** + * @} End of "defgroup field_structs". + */ + +/** + * @defgroup field_crud Field CRUD API + * @{ + * Create, update, and delete Field API fields, bundles, and instances. + * + * Modules use this API, often in hook_install(), to create custom + * data structures. The Content Construction Kit user-interface + * module uses this API for its major functionality. + * + * The Field CRUD API uses + * @link field_structs Field API data structures @endlink. + */ + +/** + * Create a field. This function does not bind the field to any + * bundle; use field_create_instance for that. + * + * @param $field + * A field structure. The field_name and type properties are required. + * @throw + * FieldException + */ +function field_create_field($field) { + // Field name is required. + if (empty($field['field_name'])) { + throw new FieldException('Attempt to create an unnamed field.'); + } + // Field name cannot contain invalid characters. + if (preg_match('/[^a-z0-9_]/', $field['field_name'])) { + throw new FieldException('Attempt to create a field with invalid characters. Only alphanumeric characters and underscores are allowed.'); + } + + // TODO: check that field_name < 32 chars. + + // Check that the field type is known. + $field_type = field_info_field_types($field['type']); + if (!$field_type) { + throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type']))); + } + + // Ensure the field name is unique. We also check disabled or deleted fields. + // TODO : do we want specific messages when clashing with a disabled or inactive field ? + $prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE, 'include_deleted' => TRUE)); + if (!empty($prior_field)) { + throw new FieldException(t('Attempt to create field name %name which already exists.', array('%name' => $field['field_name']))); + } + + $field += array( + 'cardinality' => 1, + 'locked' => FALSE, + 'settings' => array(), + ); + $module = $field_type['module']; + // Create all per-field-type properties (needed here as long as we have + // settings that impact column definitions). + $field['settings'] += field_info_field_settings($field['type']); + $field['module'] = $module; + $field['active'] = 1; + $field['deleted'] = 0; + // Create the data table. We need to populate the field columns, even though + // we don't actually store them. + $field['columns'] = (array) module_invoke($field['module'], 'field_columns', $field); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_field', $field); + + drupal_write_record('field_config', $field); + + // Clear caches + field_cache_clear(TRUE); +} + +/** + * Read a single field record directly from the database. Generally, + * you should use the field_info_field() instead. + * + * @param $field_name + * The field name to read. + * @param array $include_additional + * The default behavior of this function is to not return a field that + * is inactive or has been deleted. Setting + * $include_additional['include_inactive'] or + * $include_additional['include_deleted'] to TRUE will override this + * behavior. + * @return + * A field structure, or FALSE. + */ +function field_read_field($field_name, $include_additional = array()) { + $fields = field_read_fields(array('field_name' => $field_name), $include_additional); + return $fields ? current($fields) : FALSE; +} + +/** + * Read in fields that match an array of conditions. + * + * @param array $params + * An array of conditions to match against. + * @param array $include_additional + * The default behavior of this function is to not return fields that + * are inactive or have been deleted. Setting + * $include_additional['include_inactive'] or + * $include_additional['include_deleted'] to TRUE will override this + * behavior. + * @return + * An array of fields matching $params. + */ +function field_read_fields($params = array(), $include_additional = array()) { + $query = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC)); + $query->fields('fc'); + + // Turn the conditions into a query. + foreach ($params as $key => $value) { + $query->condition($key, $value); + } + if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) { + $query->condition('fc.active', 1); + } + if (!isset($include_additional['include_deleted']) || !$include_additional['include_deleted']) { + $query->condition('fc.deleted', 0); + } + + $fields = array(); + $results = $query->execute(); + foreach ($results as $field) { + // drupal_write_record() writes an empty string for empty arrays. + $field['settings'] = $field['settings'] ? unserialize($field['settings']) : array(); + + module_invoke_all('field_read_field', $field); + + // Populate storage columns. + $field['columns'] = (array) module_invoke($field['module'], 'field_columns', $field); + + $fields[$field['field_name']] = $field; + } + return $fields; +} + +/** + * Mark a field for deletion, including all its instances and all data + * associated with it. + * + * @param $field_name + * The field name to delete. + */ +function field_delete_field($field_name) { + // Mark the field for deletion. + db_update('field_config') + ->fields(array('deleted' => 1)) + ->condition('field_name', $field_name) + ->execute(); + + // Mark any instances of the field for deletion. + db_update('field_config_instance') + ->fields(array('deleted' => 1)) + ->condition('field_name', $field_name) + ->execute(); + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_field', $field_name); + // Clear the cache. + field_cache_clear(TRUE); +} + +/** + * Creates an instance of a field, binding it to a bundle. + * + * @param $instance + * A field instance structure. The field_name and bundle properties + * are required. + * @throw + * FieldException + */ +function field_create_instance($instance) { + // Check that the specified field exists. + $field = field_read_field($instance['field_name']); + if (empty($field)) { + throw new FieldException("Attempt to create an instance of a field that doesn't exist."); + } + + // TODO: Check that the specifed bundle exists. + + // TODO: Check that the widget type is known and can handle the field type ? + // TODO: Check that the formatters are known and can handle the field type ? + // TODO: Check that the display build modes are known for the object type ? + // Those checks should probably happen in _field_write_instance() ? + // Problem : this would mean that CCK cannot update an instance with a disabled formatter. + + // Ensure the field instance is unique. + // TODO : do we want specific messages when clashing with a disabled or inactive instance ? + $prior_instance = field_read_instance($instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE, 'include_deleted' => TRUE)); + if (!empty($prior_instance)) { + throw new FieldException('Attempt to create a field instance which already exists.'); + } + + _field_write_instance($instance); + + module_invoke_all('field_create_instance', $instance); + + // Clear caches + field_cache_clear(); + return FALSE; +} + +/* + * Update an instance of a field. + * + * @param $instance + * An associative array represeting an instance structure. The required + * keys and values are: + * field_name: The name of an existing field. + * bundle: The bundle this field belongs to. + * Any other properties specified in $instance overwrite the + * existing values for the instance. + * @throw + * FieldException + * @see field_create_instance() + */ +function field_update_instance($instance) { + // Check that the specified field exists. + $field = field_read_field($instance['field_name']); + if (empty($field)) { + throw new FieldException("Attempt to update an instance of a nonexistent field."); + } + + // Check that the field instance exists (even if it is inactive, since we + // want to be able to replace inactive widgets with new ones). + $prior_instance = field_read_instance($instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE)); + if (empty($prior_instance)) { + throw new FieldException("Attempt to update a field instance that doesn't exist."); + } + + _field_write_instance($instance, TRUE); + + // Clear caches. + field_cache_clear(); +} + +/** + * Store an instance record in the field configuration database. + * + * @param $instance + * An instance structure. + * @param $update + * Whether this is a new or existing instance. + */ +function _field_write_instance($instance, $update = FALSE) { + $field = field_read_field($instance['field_name']); + $field_type = field_info_field_types($field['type']); + + // Set defaults. + $instance += array( + 'settings' => array(), + 'display' => array(), + 'widget' => array(), + 'required' => FALSE, + 'label' => $instance['field_name'], + 'description' => '', + 'weight' => 0, + 'deleted' => 0, + ); + + // Set default instance settings. + $instance['settings'] += field_info_instance_settings($field['type']); + + // Set default widget and settings. + $instance['widget'] += array( + // TODO: what if no 'default_widget' specified ? + 'type' => $field_type['default_widget'], + 'settings' => array(), + ); + // Check widget module. + $widget_type = field_info_widget_types($instance['widget']['type']); + $widget_module = $widget_type['module']; + $widget_active = module_exists($widget_module); + $instance['widget']['settings'] += field_info_widget_settings($instance['widget']['type']); + $instance['widget']['module'] = $widget_module; + $instance['widget']['active'] = $widget_active; + + // Make sure there is at least display info for the 'full' context. + $instance['display'] += array( + 'full' => array(), + ); + // Set default display settings for each context. + foreach ($instance['display'] as $context => $display) { + $instance['display'][$context] += array( + 'label' => 'above', + 'exclude' => 0, + // TODO: what if no 'default_formatter' specified ? + 'type' => $field_type['default_formatter'], + 'settings' => array(), + ); + $formatter_type = field_info_formatter_types($instance['display'][$context]['type']); + // TODO : 'hidden' will raise PHP warnings. + $instance['display'][$context]['module'] = $formatter_type['module']; + $instance['display'][$context]['settings'] += field_info_formatter_settings($instance['display'][$context]['type']); + } + + // Create $data to contain everything from $instance that does not + // have its own column, and thus will be stored serialized. + $data = $instance; + unset($data['field_name'], $data['bundle'], $data['widget']['type'], $data['weight'], $data['deleted']); + + $record = array( + 'field_name' => $instance['field_name'], + 'bundle' => $instance['bundle'], + 'widget_type' => $instance['widget']['type'], + 'widget_module' => $widget_module, + 'widget_active' => $widget_active, + 'weight' => $instance['weight'], + 'data' => $data, + 'deleted' => $instance['deleted'], + ); + // We need to tell drupal_update_record() the primary keys to trigger an + // update. + $primary_keys = $update ? array('field_name', 'bundle') : array(); + drupal_write_record('field_config_instance', $record, $primary_keys); +} + +/** + * Read a single instance record directly from the database. Generally, + * you should use the field_info_instance() instead. + * + * @param $field_name + * The field name to read. + * @param $bundle + * The bundle to which the field is bound. + * @param array $include_additional + * The default behavior of this function is to not return an instance that + * is inactive or has been deleted. Setting + * $include_additional['include_inactive'] or + * $include_additional['include_deleted'] to TRUE will override this + * behavior. + * @return + * An instance structure, or FALSE. + */ +function field_read_instance($field_name, $bundle, $include_additional = array()) { + $instances = field_read_instances(array('field_name' => $field_name, 'bundle' => $bundle), $include_additional); + return $instances ? current($instances) : FALSE; +} + +/** + * Load a field instance. + * + * @param $param + * An array of properties to use in selecting a field + * instance. Valid keys include any column of the + * field_config_instance table. If NULL, all instances will be returned. + * @param $include_additional + * The default behavior of this function is to not return field + * instances that are inactive or have been marked deleted. Setting + * $include_additional['include_inactive'] or + * $include_additional['include_deleted'] to TRUE will override this + * behavior. + * @return + * An array of instances matching the arguments. + */ +function field_read_instances($params = array(), $include_additional = array()) { + $query = db_select('field_config_instance', 'fci', array('fetch' => PDO::FETCH_ASSOC)); + $query->join('field_config', 'fc', 'fc.field_name = fci.field_name'); + $query->fields('fci'); + #$query->fields('fc', array('type')); + + // Turn the conditions into a query. + foreach ($params as $key => $value) { + $query->condition('fci.'.$key, $value); + } + $query->condition('fc.active', 1); + if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) { + $query->condition('fci.widget_active', 1); + } + if (!isset($include_additional['include_deleted']) || !$include_additional['include_deleted']) { + $query->condition('fc.deleted', 0); + $query->condition('fci.deleted', 0); + } + + $instances = array(); + $results = $query->execute(); + + foreach ($results as $record) { + $instance = unserialize($record['data']); + $instance['field_name'] = $record['field_name']; + $instance['bundle'] = $record['bundle']; + $instance['weight'] = $record['weight']; + $instance['deleted'] = $record['deleted']; + $instance['widget']['type'] = $record['widget_type']; + $instance['widget']['module'] = $record['widget_module']; + $instance['widget']['active'] = $record['widget_active']; + + // TODO D7 : Set default widget settings, default instance settings, default display settings. + // (the modules that defined them might have changed since the instance was last saved). + + module_invoke_all('field_read_instance', $instance); + $instances[] = $instance; + } + return $instances; +} + +/** + * Mark a field instance for deletion, including all data associated with + * it. + * + * @param $field_name + * The name of the field whose instance will be deleted. + * @param $bundle + * The bundle for the instance which will be deleted. + */ +function field_delete_instance($field_name, $bundle) { + // Mark the field instance for deletion. + db_update('field_config_instance') + ->fields(array('deleted' => 1)) + ->condition('field_name', $field_name) + ->condition('bundle', $bundle) + ->execute(); + + // Mark all data associated with the field for deletion. + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_instance', $field_name, $bundle); + // Clear the cache. + field_cache_clear(); +} + +/** + * @} End of "defgroup field_crud". + */ \ No newline at end of file === added file 'modules/field/field.default.inc' --- modules/field/field.default.inc 1970-01-01 00:00:00 +0000 +++ modules/field/field.default.inc 2009-01-30 23:17:28 +0000 @@ -0,0 +1,253 @@ +field_name when we return from _field_invoke_default(). + + // TODO D7: Allow the values to be form_altered to another location, like we + // do for the form definition ($form['#fields'][$field_name]['form_path']) ? + + if (isset($form_state['values'][$field['field_name']])) { + $items = $form_state['values'][$field['field_name']]; + + // Remove the 'value' of the 'add more' button. + unset($items[$field['field_name'] .'_add_more']); + + // TODO: the above should be moved to validate time (and values saved back + // using form_set_value() ), so that hook_field_validate() works on clean data. + // Not sure we'll want what's below in validate too. + + // Reorder items to account for drag-n-drop reordering. + if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { + $items = _field_sort_items($field, $items); + } + + // Filter out empty values. + $items = field_set_empty($field, $items); + + // _field_invoke() does not add back items for fields not present in the + // original $object, so add them manually. + $object->{$field['field_name']} = $items; + } + else { + // The form did not include this field, for instance because of access + // rules: make sure any existing value for the field stays unchanged. + unset($object->{$field['field_name']}); + } +} + +/** + * The 'view' operation constructs the $object in a way that you can use + * drupal_render() to display the formatted output for an individual field. + * i.e. print drupal_render($object->content['field_foo']); + * + * The code supports both single value formatters, which theme an individual + * item value, and multiple value formatters, which theme all values for the + * field in a single theme. The multiple value formatters could be used, for + * instance, to plot field values on a single map or display them in a graph. + * Single value formatters are the default, multiple value formatters can be + * designated as such in formatter_info(). + * + * The $object array will look like: + * $object->content['field_foo']['wrapper'] = array( + * '#type' => 'field', + * '#title' => 'label' + * '#field_name' => 'field_name', + * '#object' => $object, + * '#object_type' => $obj_type, + * // Value of the $teaser param of hook_nodeapi('view'). + * '#teaser' => $teaser, + * 'items' => + * 0 => array( + * '#item' => $items[0], + * // Only for 'single-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#bundle' => $bundle, + * '#formatter' => $formatter_name, + * '#settings' => $formatter_settings, + * '#object' => $object, + * '#object_type' => $obj_type, + * '#delta' => 0, + * ), + * 1 => array( + * '#item' => $items[1], + * // Only for 'single-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#bundle' => $bundle_name, + * '#formatter' => $formatter_name, + * '#settings' => $formatter_settings, + * '#object' => $object, + * '#object_type' => $obj_type, + * '#delta' => 1, + * ), + * // Only for 'multiple-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#bundle' => $bundle_name, + * '#formatter' => $formatter_name, + * '#settings' => $formatter_settings, + * ), + * ); + */ +function field_default_view($obj_type, $object, $field, $instance, $items, $teaser) { + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); + + $addition = array(); + + // Entities without build modes should provide a 'full' context. + // NODE_BUILD_NORMAL is 0, and ('whatever' == 0) is TRUE, so we need a ===. + if (!isset($object->build_mode)) { + $context = 'full'; + } + elseif ($object->build_mode === NODE_BUILD_NORMAL + || $object->build_mode == NODE_BUILD_PREVIEW) { + $context = $teaser ? 'teaser' : 'full'; + } + else { + $context = $object->build_mode; + } + + // If we don't have specific settings for the current build_nmode, we use the + // (required) 'full' build_mode. + $display = isset($instance['display'][$context]) ? $instance['display'][$context] : $instance['display']['full']; + // Ensure we have a valid formatter and formatter settings. + $display = _field_get_formatter($display, $field); + + if ($display['type'] && $display['type'] !== 'hidden') { + $theme = 'field_formatter_'. $display['type']; + $single = (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT); + + $label_display = $display['label']; + if (isset($object->build_mode) && $object->build_mode == NODE_BUILD_SEARCH_INDEX) { + $label_display = 'hidden'; + } + + $info = array( + '#field_name' => $field['field_name'], + '#bundle' => $bundle, + '#object' => $object, + '#object_type' => $obj_type, + ); + + $element = $info + array( + '#type' => 'field', + '#title' => check_plain(t($instance['label'])), + '#access' => field_access('view', $field), + '#label_display' => $label_display, + '#teaser' => $teaser, + '#single' => $single, + 'items' => array(), + ); + + // Fill-in items. + foreach ($items as $delta => $item) { + $element['items'][$delta] = array( + '#item' => $item, + '#weight' => $delta, + ); + } + + // Append formatter information either on each item ('single-value' formatter) + // or at the upper 'items' level ('multiple-value' formatter) + $format_info = $info + array( + '#formatter' => $display['type'], + '#settings' => $display['settings'], + '#theme' => $theme, + ); + + if ($single) { + foreach ($items as $delta => $item) { + $element['items'][$delta] += $format_info; + $element['items'][$delta]['#item']['#delta'] = $delta; + } + } + else { + $element['items'] += $format_info; + } + + // The wrapper lets us get the themed output for the whole field + // to populate the $FIELD_NAME_rendered variable for templates, + // and hide it from the $content variable if needed. + // See 'preprocess' op and theme_content_field_wrapper()? + $wrapper = $info + array( + 'field' => $element, + '#weight' => $instance['weight'], + '#post_render' => array('field_wrapper_post_render'), + '#context' => $context, + ); + + $addition = array($field['field_name'] => $wrapper); + } + return $addition; +} + +/** + * Hide excluded fields from the $content variable in templates. + */ +function field_wrapper_post_render($content, $element) { + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + if (theme('field_exclude', $content, $instance, $element['#context'])) { + return ''; + } + return $content; +} + +/** + * 'Theme' function for a field's addition to the combined template output, + * i.e. the node's $content or the user's $user_profile value. + * This allows more flexibility in templates : you can use custom markup + * around a few specific fields, and print the rest normally. + * + * This is a theme function, so it can be overridden in different + * themes to produce different results. + * + * The html for individual fields and groups are available in the + * $FIELD_NAME_rendered and $GROUP_NAME_rendered variables. + * + * @return + * Whether or not the field's content is to be added in this context. + * Uses the 'exclude' value from the field's display settings. + */ +function theme_field_exclude($content, $object, $context) { + if (empty($object['display']) + || empty($object['display'][$context]) + || empty($object['display'][$context]['exclude'])) { + return FALSE; + } + else { + return TRUE; + } +} + +function field_default_preprocess($obj_type, $object, $field, $instance, &$items) { + return array( + $field['field_name'] .'_rendered' => isset($object->content[$field['field_name']]['#children']) ? $object->content[$field['field_name']]['#children'] : '', + ); +} + +function field_default_prepare_translation($obj_type, $object, $field, $instance, &$items) { + $addition = array(); + if (isset($object->translation_source->$field['field_name'])) { + $addition[$field['field_name']] = $object->translation_source->$field['field_name']; + } + return $addition; +} \ No newline at end of file === added file 'modules/field/field.form.inc' --- modules/field/field.form.inc 1970-01-01 00:00:00 +0000 +++ modules/field/field.form.inc 2009-01-31 20:12:14 +0000 @@ -0,0 +1,419 @@ + $field, + 'instance' => $instance, + ); + // TODO : why do we need this ? + $form['#cache'] = FALSE; + + // If the form is rebuilt (non-JS 'add more' button), we take the incoming + // form values. + // TODO : probably not the right fix. We'd need a field_attach_extract_form_values(), + // and call it from field_add_more_submit(). + if (isset($form_state['rebuild']) && $form_state['rebuild'] && !empty($form_state['values'][$field['field_name']])) { + $items = $form_state['values'][$field['field_name']]; + // If there was an AHAH add more button in this field, don't save it. + unset($items[$field['field_name'] .'_add_more']); + } + // Populate widgets with default values if we're creating a new object. + if (empty($items) && empty($id) && !empty($instance['default_value_function'])) { + $items = array(); + $function = $instance['default_value_function']; + if (drupal_function_exists($function)) { + $items = $function($obj_type, $object, $field, $instance); + } + } + + $form_element = array(); + + // If field module handles multiple values for this form element, + // and we are displaying an individual element, process the multiple value + // form. + if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { + $form_element = field_multiple_value_form($field, $instance, $items, $form, $form_state); + } + // If the widget is handling multiple values (e.g optionwidgets), + // or if we are displaying an individual element, just get a single form + // element and make it the $delta value. + else { + $delta = isset($get_delta) ? $get_delta : 0; + $function = $instance['widget']['module'] .'_field_widget'; + if (drupal_function_exists($function)) { + if ($element = $function($form, $form_state, $field, $instance, $items, $delta)) { + $defaults = array( + '#required' => $get_delta > 0 ? FALSE : $instance['required'], + '#columns' => array_keys($field['columns']), + '#title' => check_plain(t($instance['label'])), + '#description' => field_filter_xss($instance['description']), + '#delta' => $delta, + '#field_name' => $field['field_name'], + '#bundle' => $instance['bundle'], + ); + $element = array_merge($element, $defaults); + // If we're processing a specific delta value for a field where the + // field module handles multiples, set the delta in the result. + // For fields that handle their own processing, we can't make assumptions + // about how the field is structured, just merge in the returned value. + if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { + $form_element[$delta] = $element; + } + else { + $form_element = $element; + } + } + } + } + + if ($form_element) { + $defaults = array( + '#field_name' => $field['field_name'], + '#tree' => TRUE, + '#weight' => $instance['weight'], + ); + + $addition[$field['field_name']] = array_merge($form_element, $defaults); + $form['#fields'][$field['field_name']]['form_path'] = array($field['field_name']); + } + + return $addition; +} + +/** + * Special handling to create form elements for multiple values. + * + * Handles generic features for multiple fields: + * - number of widgets + * - AHAH-'add more' button + * - drag-n-drop value reordering + */ +function field_multiple_value_form($field, $instance, $items, &$form, &$form_state) { + $field = field_info_field($instance['field_name']); + $field_name = $field['field_name']; + + switch ($field['cardinality']) { + case FIELD_CARDINALITY_UNLIMITED: + $filled_items = field_set_empty($field, $items); + $current_item_count = isset($form_state['item_count'][$field_name]) + ? $form_state['item_count'][$field_name] + : count($items); + // We always want at least one empty icon for the user to fill in. + $max = ($current_item_count > count($filled_items)) + ? $current_item_count - 1 + : $current_item_count; + + break; + default: + $max = $field['cardinality'] - 1; + break; + } + + $title = check_plain(t($instance['label'])); + $description = field_filter_xss(t($instance['description'])); + + $form_element = array( + '#theme' => 'field_multiple_value_form', + '#multiple' => $field['cardinality'], + '#title' => $title, + '#required' => $instance['required'], + '#description' => $description, + ); + + $function = $instance['widget']['module'] .'_field_widget'; + if (drupal_function_exists($function)) { + for ($delta = 0; $delta <= $max; $delta++) { + if ($element = $function($form, $form_state, $field, $instance, $items, $delta)) { + $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; + $defaults = array( + '#title' => $multiple ? '' : $title, + '#description' => $multiple ? '' : $description, + '#required' => $delta == 0 && $instance['required'], + '#weight' => $delta, + '#delta' => $delta, + '#columns' => array_keys($field['columns']), + '#field_name' => $field_name, + '#bundle' => $instance['bundle'], + ); + + // Add an input field for the delta (drag-n-drop reordering), which will + // be hidden by tabledrag js behavior. + if ($multiple) { + // We name the element '_weight' to avoid clashing with column names + // defined by field modules. + $element['_weight'] = array( + '#type' => 'weight', + '#delta' => $max, // this 'delta' is the 'weight' element's property + '#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta, + '#weight' => 100, + ); + } + + $form_element[$delta] = array_merge($element, $defaults); + } + } + + // Add AHAH add more button, if not working with a programmed form. + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form['#programmed'])) { + // Make sure the form is cached so ahah can work. + $form['#cache'] = TRUE; + $bundle_name_url_str = str_replace('_', '-', $instance['bundle']); + $field_name_url_str = str_replace('_', '-', $field_name); + + $form_element[$field_name .'_add_more'] = array( + '#type' => 'submit', + '#name' => $field_name .'_add_more', + '#value' => t('Add another item'), + '#weight' => $instance['weight'] + $max + 1, + // Submit callback for disabled JavaScript. + '#submit' => array('field_add_more_submit'), + '#ahah' => array( + 'path' => 'field/js_add_more/'. $bundle_name_url_str .'/'. $field_name_url_str, + 'wrapper' => $field_name_url_str .'-items', + 'method' => 'replace', + 'effect' => 'fade', + ), + // When JS is disabled, the field_add_more_submit handler will find + // the relevant field using these entries. + '#field_name' => $field_name, + '#bundle' => $instance['bundle'], + ); + + // Add wrappers for the fields and 'more' button. + $form_element['#prefix'] = '
'; + $form_element[$field_name .'_add_more']['#prefix'] = '
'; + $form_element[$field_name .'_add_more']['#suffix'] = '
'; + } + } + return $form_element; +} + +/** + * Theme an individual form element. + * + * Combine multiple values into a table with drag-n-drop reordering. + * TODO : convert to a template. + */ +function theme_field_multiple_value_form($element) { + $output = ''; + + if ($element['#multiple'] > 1 || $element['#multiple'] == FIELD_CARDINALITY_UNLIMITED) { + $table_id = $element['#field_name'] .'_values'; + $order_class = $element['#field_name'] .'-delta-order'; + $required = !empty($element['#required']) ? '*' : ''; + + $header = array( + array( + 'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)), + 'colspan' => 2 + ), + t('Order'), + ); + $rows = array(); + + // Sort items according to '_weight' (needed when the form comes back after + // preview or failed validation) + $items = array(); + foreach (element_children($element) as $key) { + if ($key !== $element['#field_name'] .'_add_more') { + $items[] = &$element[$key]; + } + } + usort($items, '_field_sort_items_value_helper'); + + // Add the items as table rows. + foreach ($items as $key => $item) { + $item['_weight']['#attributes']['class'] = $order_class; + $delta_element = drupal_render($item['_weight']); + $cells = array( + array('data' => '', 'class' => 'field-multiple-drag'), + drupal_render($item), + array('data' => $delta_element, 'class' => 'delta-order'), + ); + $rows[] = array( + 'data' => $cells, + 'class' => 'draggable', + ); + } + + $output .= theme('table', $header, $rows, array('id' => $table_id, 'class' => 'field-multiple-table')); + $output .= $element['#description'] ? '
'. $element['#description'] .'
' : ''; + $output .= drupal_render($element[$element['#field_name'] .'_add_more']); + + drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); + } + else { + foreach (element_children($element) as $key) { + $output .= drupal_render($element[$key]); + } + } + + return $output; +} + +/** + * Submit handler to add more choices to a field form. This handler is used when + * JavaScript is not available. It makes changes to the form state and the + * entire form is rebuilt during the page reload. + */ +function field_add_more_submit($form, &$form_state) { + // TODO : this is straight from poll_more_choices_submit(). + // not sure what we should do here... + // Just setting $form_state['rebuild'] seems to do the trick ? + + // Set the form to rebuild and run submit handlers. + //node_form_submit_build_node($form, $form_state); + $form_state['rebuild'] = TRUE; + + $field_name = $form_state['clicked_button']['#field_name']; + + // Make the changes we want to the form state. + if ($form_state['values'][$field_name][$field_name .'_add_more']) { + $form_state['item_count'][$field_name] = count($form_state['values'][$field_name]); + } +} + +/** + * Menu callback for AHAH addition of new empty widgets. + */ +function field_add_more_js($bundle_name, $field_name) { + // Arguments are coming from the url, so we translate back dashes. + $field_name = str_replace('-', '_', $field_name); + + $invalid = FALSE; + if (empty($_POST['form_build_id'])) { + // Invalid request. + $invalid = TRUE; + } + + // Retrieve the cached form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // Invalid form_build_id. + $invalid = TRUE; + } + + // Retrieve field information. + $field = $form['#fields'][$field_name]['field']; + $instance = $form['#fields'][$field_name]['instance']; + $form_path = $form['#fields'][$field_name]['form_path']; + if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { + // Ivnalid + $invalid = TRUE; + } + + if ($invalid) { + drupal_json(array('data' => '')); + exit; + } + + // We don't simply return a new empty widget row to append to existing ones, + // because: + // - ahah.js won't simply let us add a new row to a table + // - attaching the 'draggable' behavior won't be easy + // So we resort to rebuilding the whole table of widgets including the + // existing ones, which makes us jump through a few hoops. + + // The form that we get from the cache is unbuilt. We need to build it so + // that _value callbacks can be executed and $form_state['values'] populated. + // We only want to affect $form_state['values'], not the $form itself + // (built forms aren't supposed to enter the cache) nor the rest of + // $form_state, so we use copies of $form and $form_state. + $form_copy = $form; + $form_state_copy = $form_state; + $form_copy['#post'] = array(); + form_builder($_POST['form_id'], $form_copy, $form_state_copy); + // Just grab the data we need. + $form_state['values'] = $form_state_copy['values']; + // Reset cached ids, so that they don't affect the actual form we output. + form_clean_id(NULL, TRUE); + + // Sort the $form_state['values'] we just built *and* the incoming $_POST data + // according to d-n-d reordering. + unset($form_state['values'][$field_name][$field['field_name'] .'_add_more']); + foreach ($_POST[$field_name] as $delta => $item) { + $form_state['values'][$field_name][$delta]['_weight'] = $item['_weight']; + } + $form_state['values'][$field_name] = _field_sort_items($field, $form_state['values'][$field_name]); + $_POST[$field_name] = _field_sort_items($field, $_POST[$field_name]); + + // Build our new form element for the whole field, asking for one more element. + $form_state['item_count'] = array($field_name => count($_POST[$field_name]) + 1); + $items = $form_state['values'][$field_name]; + $form_element = field_default_form(NULL, NULL, $field, $instance, $items, $form, $form_state); + // Let other modules alter it. + drupal_alter('form', $form_element, array(), 'field_add_more_js'); + + // Add the new element at the right location in the (original, unbuilt) form. + $target = &$form; + foreach ($form_path as $key) { + $target = &$target[$key]; + } + $target = $form_element[$field_name]; + + // Save the new definition of the form. + $form_state['values'] = array(); + form_set_cache($form_build_id, $form, $form_state); + + // Build the new form against the incoming $_POST values so that we can + // render the new element. + $delta = max(array_keys($_POST[$field_name])) + 1; + $_POST[$field_name][$delta]['_weight'] = $delta; + $form_state = array('submitted' => FALSE); + $form += array( + '#post' => $_POST, + '#programmed' => FALSE, + ); + $form = form_builder($_POST['form_id'], $form, $form_state); + + // Render the new output. + // We get fetch the form element from the built $form. + $field_form = $form; + foreach ($form_path as $key) { + $field_form = $field_form[$key]; + } + // We add a div around the new field to receive the ahah effect. + $field_form[$delta]['#prefix'] = '
'. (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : ''); + $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') .'
'; + + // If a newly inserted widget contains AHAH behaviors, they normally won't + // work because AHAH doesn't know about those - it just attaches to the exact + // form elements that were initially specified in the Drupal.settings object. + // The new ones didn't exist then, so we need to update Drupal.settings + // by ourselves in order to let AHAH know about those new form elements. + $javascript = drupal_add_js(NULL, NULL); + $output_js = isset($javascript['setting']) ? '' : ''; + + $output = theme('status_messages') . drupal_render($field_form) . $output_js; + drupal_json(array('status' => TRUE, 'data' => $output)); + exit; +} === added file 'modules/field/field.info' --- modules/field/field.info 1970-01-01 00:00:00 +0000 +++ modules/field/field.info 2009-01-30 23:17:27 +0000 @@ -0,0 +1,15 @@ +; $Id$ +name = Field +description = Field API to add fields to objects like nodes and users. +package = Core - fields +core = 7.x +files[] = field.module +files[] = field.install +files[] = field.crud.inc +files[] = field.info.inc +files[] = field.default.inc +files[] = field.attach.inc +files[] = field.form.inc +files[] = field.autoload.inc +dependencies[] = field_sql_storage +required = TRUE === added file 'modules/field/field.info.inc' --- modules/field/field.info.inc 1970-01-01 00:00:00 +0000 +++ modules/field/field.info.inc 2009-01-30 23:17:28 +0000 @@ -0,0 +1,492 @@ +data; + } + else { + $info = array( + 'field types' => array(), + 'widget types' => array(), + 'formatter types' => array(), + 'fieldable types' => array(), + ); + + // Populate field types. + foreach (module_implements('field_info') as $module) { + $field_types = (array) module_invoke($module, 'field_info'); + foreach ($field_types as $name => $field_info) { + $info['field types'][$name] = $field_info; + $info['field types'][$name]['module'] = $module; + } + } + + // Populate widget types. + foreach (module_implements('field_widget_info') as $module) { + $widget_types = (array) module_invoke($module, 'field_widget_info'); + foreach ($widget_types as $name => $widget_info) { + $info['widget types'][$name] = $widget_info; + $info['widget types'][$name]['module'] = $module; + } + } + + // Populate formatters. + foreach (module_implements('field_formatter_info') as $module) { + $formatter_types = (array) module_invoke($module, 'field_formatter_info'); + foreach ($formatter_types as $name => $formatter_info) { + $info['formatter types'][$name] = $formatter_info; + $info['formatter types'][$name]['module'] = $module; + } + } + + // Populate information about 'fieldable' entities. + foreach (module_implements('fieldable_info') as $module) { + $fieldable_types = (array) module_invoke($module, 'fieldable_info'); + foreach ($fieldable_types as $name => $fieldable_info) { + // Provide defaults. + $fieldable_info += array( + 'revision key' => '', + 'bundle key' => '', + 'cacheable' => TRUE, + 'bundles' => array(), + ); + // If no bundle key provided, then we assume a single bundle, named + // after the type of the object. Make sure the bundle created + // has the human-readable name we need for bundle messages. + if (empty($fieldable_info['bundle key'])) { + $fieldable_info['bundles'] = array($name => $fieldable_info['name']); + } + $info['fieldable types'][$name] = $fieldable_info; + $info['fieldable types'][$name]['module'] = $module; + } + } + + cache_set('field_info_types', $info, 'cache_field'); + } + } + + return $info; +} + +/** + * Collate all information on existing fields and instances. + * + * @param $reset + * If TRUE, clear the cache. The information will be rebuilt from the database + * next time it is needed. Defaults to FALSE. + * @return + * If $reset is TRUE, nothing. + * If $reset is FALSE, an array containing the following elements: + * + * fields: array of all defined Field objects, keyed by field name + * + * instances: array of bundled field info, keyed by bundle name + * * contains all FieldInstance objects for this bundle, keyed by field name. + */ +function _field_info_collate_fields($reset = FALSE) { + static $info; + + if ($reset) { + $info = NULL; + cache_clear_all('field_info_fields', 'cache_field'); + return; + } + + if (!isset($info)) { + if ($cached = cache_get('field_info_fields', 'cache_field')) { + $info = $cached->data; + } + else { + $info = array( + 'fields' => field_read_fields(), + 'instances' => array_fill_keys(array_keys(field_info_bundles()), array()), + ); + + // Populate instances. + $instances = field_read_instances(); + foreach ($instances as $instance) { + $info['instances'][$instance['bundle']][$instance['field_name']] = $instance; + } + + cache_set('field_info_fields', $info, 'cache_field'); + } + } + + return $info; +} + +/** + * @} End of "defgroup field_info" + * + * The rest of the functions in this file are not in a group, but + * their automatically-generated autoloaders are (see field.autoload.inc). + */ + +/** + * @autoload field_(?:behaviors|info)_.* field_info FieldException { + */ + +/** + * Helper function for determining the behavior of a field + * with respect to a given operation. + * + * @param $op + * The name of the operation. + * Currently supported : none + * // TODO D7: no use cases (yet ?) - do we want to keep that function ?. + * @param $field + * The field array. + * @return + * FIELD_BEHAVIOR_NONE - do nothing for this operation. + * FIELD_BEHAVIOR_CUSTOM - use the field's callback function. + * FIELD_BEHAVIOR_DEFAULT - use field.module default behavior. + */ +function _field_behaviors_field($op, $field) { + $info = field_info_field_types($field['type']); + return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT; +} + +/** + * Helper function for determining the behavior of a widget + * with respect to a given operation. + * + * @param $op + * The name of the operation. + * Currently supported: 'default value', 'multiple values'. + * @param $instance + * The field instance array. + * @return + * FIELD_BEHAVIOR_NONE - do nothing for this operation. + * FIELD_BEHAVIOR_CUSTOM - use the widget's callback function. + * FIELD_BEHAVIOR_DEFAULT - use field.module default behavior. + */ +function _field_behaviors_widget($op, $instance) { + $info = field_info_widget_types($instance['widget']['type']); + return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT; +} + +/** + * Helper function for determining the behavior of a formatter + * with respect to a given operation. + * + * @param $op + * The name of the operation. + * Currently supported: 'multiple values' + * @param $display + * The $instance['display'][$build_mode] array. + * @return + * FIELD_BEHAVIOR_NONE - do nothing for this operation. + * FIELD_BEHAVIOR_CUSTOM - use the formatter's callback function. + * FIELD_BEHAVIOR_DEFAULT - use field module default behavior. + */ +function _field_behaviors_formatter($op, $display) { + $info = field_info_formatter_types($display['type']); + return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT; +} + +/** + * Return hook_field_info() data. + * + * @param $field_type + * (optional) A field type name. If ommitted, all field types will be + * returned. + * @return + * Either a field type description, as provided by hook_field_info(), or an + * array of all existing field types, keyed by field type name. + */ +function _field_info_field_types($field_type = NULL) { + $info = _field_info_collate_types(); + $field_types = $info['field types']; + if ($field_type) { + if (isset($field_types[$field_type])) { + return $field_types[$field_type]; + } + } + else { + return $field_types; + } +} + +/** + * Return hook_field_widget_info() data. + * + * @param $widget_type + * (optional) A widget type name. If ommitted, all widget types will be + * returned. + * @return + * Either a widget type description, as provided by + * hook_field_widget_info(), or an array of all existing widget + * types, keyed by widget type name. + */ +function _field_info_widget_types($widget_type = NULL) { + $info = _field_info_collate_types(); + $widget_types = $info['widget types']; + if ($widget_type) { + if (isset($widget_types[$widget_type])) { + return $widget_types[$widget_type]; + } + } + else { + return $widget_types; + } +} + +/** + * Return hook_field_formatter_info() data. + * + * @param $formatter_type + * (optional) A formatter type name. If ommitted, all formatter types will be + * returned. + * @return + * Either a formatter type description, as provided by hook_field_formatter_info(), + * or an array of all existing widget types, keyed by widget type name. + */ +function _field_info_formatter_types($formatter_type = NULL) { + $info = _field_info_collate_types(); + $formatter_types = $info['formatter types']; + if ($formatter_type) { + if (isset($formatter_types[$formatter_type])) { + return $formatter_types[$formatter_type]; + } + } + else { + return $formatter_types; + } +} + +/** + * Return hook_fieldable_info() data. + * + * @param $obj_type + * (optional) A fieldable type name. If ommitted, all fieldable types will be + * returned. + * @return + * Either a fieldable type description, as provided by hook_fieldable_info(), + * or an array of all existing fieldable types, keyed by fieldable type name. + */ +function _field_info_fieldable_types($obj_type = NULL) { + $info = _field_info_collate_types(); + $fieldable_types = $info['fieldable types']; + if ($obj_type) { + if (isset($fieldable_types[$obj_type])) { + return $fieldable_types[$obj_type]; + } + } + else { + return $fieldable_types; + } +} + +/** + * Return an array of fieldable bundle names and labels, for an individual + * object type or for all object types. + */ +function _field_info_bundles($obj_type = NULL) { + $info = _field_info_collate_types(); + $bundles = array(); + foreach ($info['fieldable types'] as $type => $fieldable_info) { + if (empty($obj_type) || $obj_type == $type) { + $bundles += $fieldable_info['bundles']; + } + } + return $bundles; +} + +/** + * Identity the type of entity that created a bundle. + * // TODO : might not be needed depending on how we solve + * // the 'namespace bundle names' issue + */ +function _field_info_bundle_entity($bundle) { + $info = _field_info_collate_types(); + foreach ($info['fieldable types'] as $type => $fieldable_info) { + if (isset($fieldable_info['bundles'][$bundle])) { + return $type; + } + } + return FALSE; +} + +/** + * Return array of all field data, keyed by field name. + * + * @return + * An array of Field objects. + */ +function _field_info_fields() { + $info = _field_info_collate_fields(); + return $info['fields']; +} + +/** + * Return data about an individual field. + * + * @param $field_name + */ +function _field_info_field($field_name) { + $info = _field_info_collate_fields(); + if (isset($info['fields'][$field_name])) { + return $info['fields'][$field_name]; + } +} + + +/** + * Return an array of instance data for a given bundle, + * or for all known bundles, keyed by bundle name and field name. + * + * @param $bundle_name + * If set, return information on just this bundle. + */ +function _field_info_instances($bundle_name = NULL) { + $info = _field_info_collate_fields(); + if (!isset($bundle_name)) { + return $info['instances']; + } + if (isset($info['instances'][$bundle_name])) { + return $info['instances'][$bundle_name]; + } + return array(); +} + +/** + * Return an array of instance data for a specific field and bundle. + */ +function _field_info_instance($field_name, $bundle_name) { + $info = _field_info_collate_fields(); + if (isset($info['instances'][$bundle_name][$field_name])) { + return $info['instances'][$bundle_name][$field_name]; + } +} + +/** + * Return a field type's default settings. + * + * @param $type + * A field type name. + * @return + * The field type's default settings, as provided by hook_field_info(), or an + * empty array. + */ +function _field_info_field_settings($type) { + $info = field_info_field_types($type); + return isset($info['settings']) ? $info['settings'] : array(); +} + +/** + * Return a field type's default instance settings. + * + * @param $type + * A field type name. + * @return + * The field type's default instance settings, as provided by + * hook_field_info(), or an empty array. + */ +function _field_info_instance_settings($type) { + $info = field_info_field_types($type); + return isset($info['instance_settings']) ? $info['instance_settings'] : array(); +} + +/** + * Return a field widget's default settings. + * + * @param $type + * A widget type name. + * @return + * The field type's default settings, as provided by hook_field_info(), or an + * empty array. + */ +function _field_info_widget_settings($type) { + $info = field_info_widget_types($type); + return isset($info['settings']) ? $info['settings'] : array(); +} + +/** + * Return a field formatter's default settings. + * + * @param $type + * A field formatter type name. + * @return + * The field formatter's default settings, as provided by + * hook_field_info(), or an empty array. + */ +function _field_info_formatter_settings($type) { + $info = field_info_formatter_types($type); + return isset($info['settings']) ? $info['settings'] : array(); +} + +/** + * @autoload} End of "@autoload field_info" + */ === added file 'modules/field/field.install' --- modules/field/field.install 1970-01-01 00:00:00 +0000 +++ modules/field/field.install 2009-01-30 23:17:27 +0000 @@ -0,0 +1,154 @@ + array( + 'field_name' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => 'The name of this field', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'The type of this field, coming from a field module', + ), + 'locked' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => '@TODO', + ), + 'settings' => array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => TRUE, + 'serialize' => TRUE, + 'description' => 'Field specific settings, for example maximum length', + ), + 'module' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'cardinality' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'active' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('field_name'), + 'indexes' => array( + // used by field_read_fields + 'active_deleted' => array('active', 'deleted'), + // used by field_modules_disabled + 'module' => array('module'), + // used by field_associate_fields + 'type' => array('type'), + ), + ); + $schema['field_config_instance'] = array( + 'fields' => array( + 'field_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'bundle' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), + 'widget_type' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), + 'widget_module' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), + 'widget_active' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + 'data' => array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => TRUE, + 'serialize' => TRUE, + ), + 'weight' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('field_name', 'bundle'), + 'indexes' => array( + // used by field_read_instances + 'widget_active_deleted' => array('widget_active', 'deleted'), + // used by field_modules_disabled + 'widget_module' => array('widget_module'), + // used by field_associate_fields + 'widget_type' => array('widget_type'), + ), + ); + $schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache'); + + return $schema; +} === added file 'modules/field/field.module' --- modules/field/field.module 1970-01-01 00:00:00 +0000 +++ modules/field/field.module 2009-01-30 23:17:27 +0000 @@ -0,0 +1,673 @@ +' . t('The Field API allows custom data fields to be attached to Drupal objects and takes care of storing, loading, editing, and rendering field data. Any object type (node, user, etc.) can use the Field API to make itself "fieldable" and thus allow fields to be attached to it.') . '

'; + $output .= '

' .t('The Field API provides no user interface on its own. Use the Content Construction Kit (CCK) contrib module to manage custom fields via a web browser.') . '

'; + return $output; + } +} + +/** + * Implementation of hook_init(). + * + * TODO D7: Determine which functions need to always be "loaded", and + * put autoloaders for them into field.autoload.inc. Also figure out + * how to make this work during installation. + */ +function field_init() { + module_load_include('inc', 'field', 'field.crud'); + module_load_include('inc', 'field', 'field.autoload'); +} + +/** + * Implementation of hook_menu(). + */ +function field_menu() { + $items = array(); + + // Callback for AHAH add more buttons. + $items['field/js_add_more'] = array( + 'page callback' => 'field_add_more_js', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implementation of hook elements(). + */ +function field_elements() { + return array( + 'field' => array(), + ); +} + +/** + * Implementation of hook_theme(). + */ +function field_theme() { + $path = drupal_get_path('module', 'field') .'/theme'; + + return array( + 'field' => array( + 'template' => 'field', + 'arguments' => array('element' => NULL), + 'path' => $path, + ), + // TODO D7 : do we need that in core ? + // Probably no easy way for a contrib module to do this + // since the Field module creates and processes the template, + // so maybe it must be in core. In core for now. + // [yched]: This is just adding + // '#post_render' => array('field_wrapper_post_render') + // at the right places in the render array generated by field_default_view(). + // Can be done in hook_field_attach_post_view if we want. + 'field_exclude' => array( + 'arguments' => array('content' => NULL, 'object' => array(), 'context' => NULL), + ), + 'field_multiple_value_form' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implementation of hook_modules_installed(). + */ +function field_modules_installed($modules) { + field_cache_clear(); +} + +/** + * Implementation of hook_modules_uninstalled(). + */ +function field_modules_uninstalled($modules) { + module_load_include('inc', 'field', 'field.crud'); + foreach ($modules as $module) { + // TODO D7: field_module_delete is not yet implemented + // field_module_delete($module); + } +} + +/** + * Implementation of hook_modules_enabled(). + */ +function field_modules_enabled($modules) { + foreach ($modules as $module) { + field_associate_fields($module); + } + field_cache_clear(); +} + +/** + * Implementation of hook_modules_disabled(). + */ +function field_modules_disabled($modules) { + foreach ($modules as $module) { + db_update('field_config') + ->fields(array('active' => 0)) + ->condition('module', $module) + ->execute(); + db_update('field_config_instance') + ->fields(array('widget_active' => 0)) + ->condition('widget_module', $module) + ->execute(); + field_cache_clear(TRUE); + } +} + +/** + * Allows a module to update the database for fields and columns it controls. + * + * @param string $module + * The name of the module to update on. + */ +function field_associate_fields($module) { + $module_fields = module_invoke($module, 'field_info'); + if ($module_fields) { + foreach ($module_fields as $name => $field_info) { + watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module)); + db_update('field_config') + ->fields(array('module' => $module, 'active' => 1)) + ->condition('type', $name) + ->execute(); + } + } + $module_widgets = module_invoke($module, 'widget_info'); + if ($module_widgets) { + foreach ($module_widgets as $name => $widget_info) { + watchdog('field', 'Updating widget type %type with module %module.', array('%type' => $name, '%module' => $module)); + db_update('field_config_instance') + ->fields(array('widget_module' => $module, 'widget_active' => 1)) + ->condition('widget_type', $name) + ->execute(); + } + } +} + +/** + * Helper function to filter out empty values. + * + * On order to keep marker rows in the database, the function ensures + * that the right number of 'all columns NULL' values is kept. + * + * @param array $field + * @param array $items + * @return array + * returns filtered and adjusted item array + * + * TODO D7: poorly named... + */ +function field_set_empty($field, $items) { + // Filter out empty values. + $filtered = array(); + $function = $field['module'] .'_field_is_empty'; + foreach ((array) $items as $delta => $item) { + if (!$function($item, $field)) { + $filtered[] = $item; + } + } + return $filtered; +} + +/** + * Helper function to sort items in a field according to + * user drag-n-drop reordering. + */ +function _field_sort_items($field, $items) { + if (($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) && isset($items[0]['_weight'])) { + usort($items, '_field_sort_items_helper'); + foreach ($items as $delta => $item) { + if (is_array($items[$delta])) { + unset($items[$delta]['_weight']); + } + } + } + return $items; +} + +/** + * Sort function for items order. + * (copied form element_sort(), which acts on #weight keys) + */ +function _field_sort_items_helper($a, $b) { + $a_weight = (is_array($a) && isset($a['_weight'])) ? $a['_weight'] : 0; + $b_weight = (is_array($b) && isset($b['_weight'])) ? $b['_weight'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Same as above, using ['_weight']['#value'] + */ +function _field_sort_items_value_helper($a, $b) { + $a_weight = (is_array($a) && isset($a['_weight']['#value'])) ? $a['_weight']['#value'] : 0; + $b_weight = (is_array($b) && isset($b['_weight']['#value'])) ? $b['_weight']['#value'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Registry of available build modes. + * TODO : move into hook_fieldable_info() ? + */ +function field_build_modes($obj_type) { + static $info; + + if (!isset($info[$obj_type])) { + // module_invoke_all messes numeric keys. + // TODO : revisit when we move away from numeric build modes. + $info[$obj_type] = array(); + foreach (module_implements('field_build_modes') as $module) { + $info[$obj_type] += module_invoke($module, 'field_build_modes', $obj_type); + } + } + return $info[$obj_type]; +} + +/** + * Clear the cached information; called in several places when field + * information is changed. + */ +function field_cache_clear($rebuild_schema = FALSE) { + cache_clear_all('*', 'cache_field', TRUE); + + module_load_include('inc', 'field', 'field.info'); + _field_info_collate_types(TRUE); + _field_info_collate_fields(TRUE); + + // Refresh the schema to pick up new information. + // TODO : if db storage gets abstracted out, we'll need to revisit how and when + // we refresh the schema... + if ($rebuild_schema) { + $schema = drupal_get_schema(NULL, TRUE); + } +} + +/** + * Manipulate a 2D array to reverse rows and columns. + * + * // TODO D7 : do we need this ? Seems specific to option_widgets + * The default data storage for fields is delta first, column names second. + * This is sometimes inconvenient for field modules, so this function can be + * used to present the data in an alternate format. + * + * @param $array + * The array to be transposed. It must be at least two-dimensional, and + * the subarrays must all have the same keys or behavior is undefined. + * @return + * The transposed array. + */ +function field_transpose_array_rows_cols($array) { + $result = array(); + if (is_array($array)) { + foreach ($array as $key1 => $value1) { + if (is_array($value1)) { + foreach ($value1 as $key2 => $value2) { + if (!isset($result[$key2])) { + $result[$key2] = array(); + } + $result[$key2][$key1] = $value2; + } + } + } + } + return $result; +} + +/** + * Like filter_xss_admin(), but with a shorter list of allowed tags. + * + * Used for items entered by administrators, like field descriptions, + * allowed values, where some (mainly inline) mark-up may be desired + * (so check_plain() is not acceptable). + */ +function field_filter_xss($string) { + return filter_xss($string, _field_filter_xss_allowed_tags()); +} + +/** + * List of tags allowed by field_filter_xss(). + */ +function _field_filter_xss_allowed_tags() { + return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img'); +} + +/** + * Human-readable list of allowed tags, for display in help texts. + */ +function _field_filter_xss_display_allowed_tags() { + return '<'. implode('> <', _field_filter_xss_allowed_tags()) .'>'; +} + +/** + * Format a field item for display. + * + * TODO D7 : do we still need field_format ? + * - backwards compatibility of templates - check what fallbacks we can propose... + * - used by Views integration in CCK D6 + * At least needs a little rehaul/update... + * + * Used to display a field's values outside the context of the $node, as + * when fields are displayed in Views, or to display a field in a template + * using a different formatter than the one set up on the Display Fields tab + * for the node's context. + * + * @param $field + * Either a field array or the name of the field. + * @param $item + * The field item(s) to be formatted (such as $node->field_foo[0], + * or $node->field_foo if the formatter handles multiple values itself) + * @param $formatter_name + * The name of the formatter to use. + * @param $node + * Optionally, the containing node object for context purposes and + * field-instance options. + * + * @return + * A string containing the contents of the field item(s) sanitized for display. + * It will have been passed through the necessary check_plain() or check_markup() + * functions as necessary. + */ +function field_format($obj_type, $object, $field, $item, $formatter_name = NULL, $formatter_settings = array()) { + if (!is_array($field)) { + $field = field_info_field($field); + } + + if (field_access('view', $field)) { + // Basically, we need $field, $instance, $obj_type, $object to be able to display a value... + list(, , $bundle) = field_attach_extract_ids($obj_type, $object); + $instance = field_info_instance($field['field_name'], $bundle); + + $display = array( + 'type' => $formatter_name, + 'settings' => $formatter_settings, + ); + $display = _field_get_formatter($display, $field); + if ($display['type'] && $display['type'] !== 'hidden') { + $theme = $formatter['module'] .'_formatter_'. $display['type']; + + $element = array( + '#theme' => $theme, + '#field_name' => $field['field_name'], + '#bundle' => $bundle, + '#formatter' => $display['type'], + '#settings' => $display['settings'], + '#object' => $object, + '#delta' => isset($item['#delta']) ? $item['#delta'] : NULL, + ); + + if (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT) { + // Single value formatter. + + // hook_field('sanitize') expects an array of items, so we build one. + $items = array($item); + $function = $field['module'] .'_field_sanitize'; + if (function_exists($function)) { + $function($obj_type, $object, $field, $instance, $items); + } + + $element['#item'] = $items[0]; + } + else { + // Multiple values formatter. + $items = $item; + $function = $field['module'] .'_field_sanitize'; + if (function_exists($function)) { + $function($obj_type, $object, $field, $instance, $items); + } + + foreach ($items as $delta => $item) { + $element[$delta] = array( + '#item' => $item, + '#weight' => $delta, + ); + } + } + + return theme($theme, $element); + } + } +} + +/** + * Render a single field, fully themed with label and multiple values. + * + * To be used by third-party code (Views, Panels...) that needs to output + * an isolated field. Do *not* use inside node templates, use the + * $FIELD_NAME_rendered variables instead. + * + * By default, the field is displayed using the settings defined for the + * 'full' or 'teaser' contexts (depending on the value of the $teaser param). + * Set $node->build_mode to a different value to use a different context. + * + * Different settings can be specified by adjusting $field['display']. + * + * @param $field + * The field definition. + * @param $object + * The object containing the field to display. Must at least contain the id key, + * revision key (if applicable), bundle key, and the field data. + * @param $teaser + * Similar to hook_nodeapi('view') + * @return + * The themed output for the field. + */ +function field_view_field($obj_type, $object, $field, $instance, $teaser = FALSE) { + $output = ''; + if (isset($object->$field['field_name'])) { + $items = $object->$field['field_name']; + + // Use 'full'/'teaser' if not specified otherwise. + $object->build_mode = isset($object->build_mode) ? $object->build_mode : NODE_BUILD_NORMAL; + + // One-field equivalent to _field_invoke('sanitize'). + $function = $field['module'] .'_field_sanitize'; + if (drupal_function_exists($function)) { + $function($obj_type, $object, $field, $instance, $items); + $object->$field['field_name'] = $items; + } + + $view = field_default_view($obj_type, $object, $field, $instance, $items, $teaser); + // TODO : what about hook_field_attach_view ? + + // field_default_view() adds a wrapper to handle variables and 'excluded' + // fields for node templates. We bypass it and render the actual field. + $output = drupal_render($view[$field['field_name']]['field']); + } + return $output; +} + +/** + * Determine whether the user has access to a given field. + * + * @param $op + * The operation to be performed. Possible values: + * - "edit" + * - "view" + * @param $field + * The field on which the operation is to be performed. + * @param $account + * (optional) The account to check, if not given use currently logged in user. + * @return + * TRUE if the operation is allowed; + * FALSE if the operation is denied. + */ +function field_access($op, $field, $account = NULL) { + global $user; + + if (is_null($account)) { + $account = $user; + } + + $field_access = module_invoke_all('field_access', $op, $field, $account); + foreach ($field_access as $value) { + if ($value === FALSE) { + return FALSE; + } + } + return TRUE; +} + +/** + * Theme preprocess function for field.tpl.php. + * + * The $variables array contains the following arguments: + * - $object + * - $field + * - $items + * - $teaser + * - $page + * + * @see field.tpl.php + */ +function template_preprocess_field(&$variables) { + $element = $variables['element']; + list(, , $bundle) = field_attach_extract_ids($element['#object_type'], $element['#object']); + $instance = field_info_instance($element['#field_name'], $bundle); + $field = field_info_field($element['#field_name']); + + $variables['object'] = $element['#object']; + $variables['field'] = $field; + $variables['instance'] = $instance; + $variables['items'] = array(); + + if ($element['#single']) { + // Single value formatter. + foreach (element_children($element['items']) as $delta) { + $variables['items'][$delta] = $element['items'][$delta]['#item']; + // Use isset() to avoid undefined index message on #children when field values are empty. + $variables['items'][$delta]['view'] = isset($element['items'][$delta]['#children']) ? $element['items'][$delta]['#children'] : ''; + } + } + else { + // Multiple values formatter. + // We display the 'all items' output as $items[0], as if it was the + // output of a single valued field. + // Raw values are still exposed for all items. + foreach (element_children($element['items']) as $delta) { + $variables['items'][$delta] = $element['items'][$delta]['#item']; + } + $variables['items'][0]['view'] = $element['items']['#children']; + } + + $variables['teaser'] = $element['#teaser']; + $variables['page'] = (bool)menu_get_object(); + + $field_empty = TRUE; + + foreach ($variables['items'] as $delta => $item) { + if (!isset($item['view']) || (empty($item['view']) && (string)$item['view'] !== '0')) { + $variables['items'][$delta]['empty'] = TRUE; + } + else { + $field_empty = FALSE; + $variables['items'][$delta]['empty'] = FALSE; + } + } + + $additions = array( + 'field_type' => $field['type'], + 'field_name' => $field['field_name'], + 'field_type_css' => strtr($field['type'], '_', '-'), + 'field_name_css' => strtr($field['field_name'], '_', '-'), + 'label' => check_plain(t($instance['label'])), + 'label_display' => $element['#label_display'], + 'field_empty' => $field_empty, + 'template_files' => array( + 'field', + 'field-'. $element['#field_name'], + 'field-'. $bundle, + 'field-'. $element['#field_name'] .'-'. $bundle, + ), + ); + $variables = array_merge($variables, $additions); +} + +// TODO D7: needed ? +/** + * Helper function to identify inactive fields. + */ +function field_inactive_fields($type_name = NULL) { +// module_load_include('inc', 'field', 'includes/field.crud'); +// if (!empty($type_name)) { +// $param = array('type_name' => $type_name); +// $inactive = array($type_name => array()); +// } +// else { +// $param = array(); +// $inactive = array(); +// } +// $all = field_field_instance_read($param, TRUE); +// $active = array_keys(field_fields()); +// foreach ($all as $field) { +// if (!in_array($field['field_name'], $active)) { +// $inactive[$field['type_name']][$field['field_name']] = field_field_instance_expand($field); +// } +// } +// if (!empty($type_name)) { +// return $inactive[$type_name]; +// } +// return $inactive; +} + +/** + * @} End of "defgroup field" + */ \ No newline at end of file === added file 'modules/field/field.test' --- modules/field/field.test 1970-01-01 00:00:00 +0000 +++ modules/field/field.test 2009-01-31 20:12:14 +0000 @@ -0,0 +1,1171 @@ + t('Field Attach tests'), + 'description' => t("Test Field Attach API functions."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + + $this->field_name = strtolower($this->randomName().'_field_name'); + $this->table = _field_sql_storage_tablename($this->field_name); + $this->revision_table = _field_sql_storage_revision_tablename($this->field_name); + $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => 'test_bundle', + 'label' => $this->randomName().'_label', + 'description' => $this->randomName().'_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + field_create_instance($this->instance); + } + + function testFieldAttachLoad() { + $entity_type = 'test_entity'; + $eid = 0; + + $etid = _field_sql_storage_etid($entity_type); + $columns = array('etid', 'entity_id', 'revision_id', 'delta', $this->field_name .'_value'); + + // Insert data for four revisions to the field revisions table + $query = db_insert($this->revision_table)->fields($columns); + for ($evid = 0; $evid < 4; ++$evid) { + $values[$evid] = array(); + // Note: we insert one extra value ('<=' instead of '<'). + for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { + $value = mt_rand(0, 127); + $values[$evid][] = $value; + $query->values(array($etid, $eid, $evid, $delta, $value)); + } + } + $query->execute(); + + // Insert data for the "most current revision" into the field table + $query = db_insert($this->table)->fields($columns); + foreach ($values[0] as $delta => $value) { + $query->values(array($etid, $eid, 0, $delta, $value)); + } + $query->execute(); + + // Load the "most current revision" + $entity = field_test_create_stub_entity($eid, 0, $this->instance['bundle']); + field_attach_load($entity_type, array($eid => $entity)); + foreach ($values[0] as $delta => $value) { + if ($delta < $this->field['cardinality']) { + $this->assertEqual($entity->{$this->field_name}[$delta]['value'], $value, "Value $delta is loaded correctly for current revision"); + } + else { + $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}), "No extraneous value gets loaded for current revision."); + } + } + + // Load every revision + for ($evid = 0; $evid < 4; ++$evid) { + $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); + field_attach_load_revision($entity_type, array($eid => $entity)); + foreach ($values[$evid] as $delta => $value) { + if ($delta < $this->field['cardinality']) { + $this->assertEqual($entity->{$this->field_name}[$delta]['value'], $value, "Value $delta for revision $evid is loaded correctly"); + } + else { + $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}), "No extraneous value gets loaded for revision $evid."); + } + } + } + } + +// function testFieldAttachLoadMultiple() { + // TODO : test the 'multiple' aspect of load: + // define 2 bundles, 3 fields + // bundle1 gets instances of field1, field2 + // bundle2 gets instances of field1, field3 + // load 2 entities (one for each bundle) in a single load + // check that everything gets loaded ok. +// } + + function testFieldAttachInsertAndUpdate() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Test insert. + $values = array(); + // Note: we try to insert one extra value ('<=' instead of '<'). + // TODO : test empty values filtering and "compression" (store consecutive deltas). + for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $rev_values[0] = $values; + field_attach_insert($entity_type, $entity); + + $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); + foreach ($values as $delta => $value) { + if ($delta < $this->field['cardinality']) { + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Value $delta is inserted correctly")); + } + else { + $this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets inserted."); + } + } + + // Test update. + $entity = field_test_create_stub_entity(0, 1, $this->instance['bundle']); + $values = array(); + // Note: we try to update one extra value ('<=' instead of '<'). + for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $rev_values[1] = $values; + field_attach_update($entity_type, $entity); + $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); + foreach ($values as $delta => $value) { + if ($delta < $this->field['cardinality']) { + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Value $delta is updated correctly")); + } + else { + $this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets updated."); + } + } + + // Check that data for both revisions are in the revision table. + // We make sure each value is stored correctly, then unset it. + // When an entire revision's values are unset (remembering that we + // put one extra value in $values per revision), unset the entire + // revision. Then, if $rev_values is empty at the end, all + // revision data was found. + $results = db_select($this->revision_table, 't')->fields('t')->execute(); + foreach ($results as $row) { + $this->assertEqual($row->{$this->field_name . '_value'}, $rev_values[$row->revision_id][$row->delta]['value'], "Value {$row->delta} for revision {$row->revision_id} stored correctly"); + unset($rev_values[$row->revision_id][$row->delta]); + if (count($rev_values[$row->revision_id]) == 1) { + unset($rev_values[$row->revision_id]); + } + } + $this->assertTrue(empty($rev_values), "All values for all revisions are stored in revision table {$this->revision_table}"); + + // Check that update leaves the field data untouched if $object has no + // $field_name key. + unset($entity->{$this->field_name}); + field_attach_update($entity_type, $entity); + $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); + foreach ($values as $delta => $value) { + if ($delta < $this->field['cardinality']) { + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Update with no field_name entry leaves value $delta untouched")); + } + } + + // Check that update with an empty $object->$field_name empties the field. + $entity->{$this->field_name} = NULL; + field_attach_update($entity_type, $entity); + $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); + $this->assertEqual(count($rows), 0, t("Update with an empty field_name entry empties the field.")); + } + + // Test insert and update with missing or invalid fields. For the + // most part, these tests pass by not crashing or causing exceptions. + function testFieldAttachSaveMissingData() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Insert: Field is missing + field_attach_insert($entity_type, $entity); + $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); + $this->assertEqual($count, 0, 'Missing field results in no inserts'); + + // Insert: Field is NULL + $entity->{$this->field_name} = NULL; + field_attach_insert($entity_type, $entity); + $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); + $this->assertEqual($count, 0, 'NULL field results in no inserts'); + + // Add some real data + $entity->{$this->field_name} = array(0 => array('value' => 1)); + field_attach_insert($entity_type, $entity); + $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); + $this->assertEqual($count, 1, 'Field data saved'); + + // Update: Field is missing. Data should survive. + unset($entity->{$this->field_name}); + field_attach_update($entity_type, $entity); + $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); + $this->assertEqual($count, 1, 'Missing field leaves data in table'); + + // Update: Field is NULL Data should be wiped. + $entity->{$this->field_name} = NULL; + field_attach_update($entity_type, $entity); + $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); + $this->assertEqual($count, 0, 'NULL field leaves no data in table'); + } + + function testFieldAttachViewAndPreprocess() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Populate values to be displayed. + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $values; + + // Simple formatter, label displayed. + $formatter_setting = $this->randomName(); + $this->instance['display'] = array( + 'full' => array( + 'label' => 'above', + 'type' => 'field_test_default', + 'settings' => array( + 'test_formatter_setting' => $formatter_setting, + ) + ), + ); + field_update_instance($this->instance); + $entity->content = field_attach_view($entity_type, $entity); + $output = drupal_render($entity->content); + $variables = field_attach_preprocess($entity_type, $entity); + $variable = $this->instance['field_name'] . '_rendered'; + $this->assertTrue(isset($variables[$variable]), "Variable $variable is available in templates."); + $this->content = $output; + $this->assertRaw($this->instance['label'], "Label is displayed."); + $this->content = $variables[$variable]; + $this->assertRaw($this->instance['label'], "Label is displayed (template variable)."); + foreach ($values as $delta => $value) { + $this->content = $output; + $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + $this->content = $variables[$variable]; + $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied (template variable)."); + } + + // Label hidden. + $this->instance['display']['full']['label'] = 'hidden'; + field_update_instance($this->instance); + $entity->content = field_attach_view($entity_type, $entity); + $output = drupal_render($entity->content); + $variables = field_attach_preprocess($entity_type, $entity); + $this->content = $output; + $this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed."); + $this->content = $variables[$variable]; + $this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed (template variable)."); + + // Field hidden. + $this->instance['display'] = array( + 'full' => array( + 'label' => 'above', + 'type' => 'hidden', + + ), + ); + field_update_instance($this->instance); + $entity->content = field_attach_view($entity_type, $entity); + $output = drupal_render($entity->content); + $variables = field_attach_preprocess($entity_type, $entity); + $this->assertTrue(isset($variables[$variable]), "Hidden field: variable $variable is available in templates."); + $this->content = $output; + $this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed."); + foreach ($values as $delta => $value) { + $this->assertNoRaw($value['value'], "Hidden field: value $delta is not displayed."); + } + + // Multiple formatter. + $formatter_setting = $this->randomName(); + $this->instance['display'] = array( + 'full' => array( + 'label' => 'above', + 'type' => 'field_test_multiple', + 'settings' => array( + 'test_formatter_setting_multiple' => $formatter_setting, + ) + ), + ); + field_update_instance($this->instance); + $entity->content = field_attach_view($entity_type, $entity); + $output = drupal_render($entity->content); + $variables = field_attach_preprocess($entity_type, $entity); + $display = $formatter_setting; + foreach ($values as $delta => $value) { + $display .= "|$delta:{$value['value']}"; + } + $this->content = $output; + $this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied."); + $this->content = $variables[$variable]; + $this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied (template variable)."); + + // TODO: + // - check that the 'exclude' option works (if we keep it in core) + // - check display order with several fields + } + + function testFieldAttachDelete() { + $entity_type = 'test_entity'; + $rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Create revision 0 + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $rev[0]->{$this->field_name} = $values; + field_attach_insert($entity_type, $rev[0]); + + // Create revision 1 + $rev[1] = field_test_create_stub_entity(0, 1, $this->instance['bundle']); + $rev[1]->{$this->field_name} = $values; + field_attach_update($entity_type, $rev[1]); + + // Create revision 2 + $rev[2] = field_test_create_stub_entity(0, 2, $this->instance['bundle']); + $rev[2]->{$this->field_name} = $values; + field_attach_update($entity_type, $rev[2]); + + // Confirm each revision loads + foreach (array_keys($rev) as $vid) { + $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']); + field_attach_load_revision($entity_type, array(0 => $read)); + $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); + } + + // Delete revision 1, confirm the other two still load. + field_attach_delete_revision($entity_type, $rev[1]); + foreach (array(0, 2) as $vid) { + $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']); + field_attach_load_revision($entity_type, array(0 => $read)); + $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); + } + + // Confirm the current revision still loads + $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $read)); + $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object current revision has {$this->field['cardinality']} values."); + + // Delete all field data, confirm nothing loads + field_attach_delete($entity_type, $rev[2]); + foreach (array(0, 1, 2) as $vid) { + $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']); + field_attach_load_revision($entity_type, array(0 => $read)); + $this->assertFalse(isset($read->{$this->field_name}), "The test object revision $vid is deleted."); + } + $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $read)); + $this->assertFalse(isset($read->{$this->field_name}), "The test object current revision is deleted."); + } + + function testFieldAttachCreateRenameBundle() { + // Create a new bundle. This has to be initiated by the module so that its + // hook_fieldable_info() is consistent. + $new_bundle = 'test_bundle_' . strtolower($this->randomName()); + field_test_create_bundle($new_bundle, $this->randomName()); + + // Add an instance to that bundle. + $this->instance['bundle'] = $new_bundle; + field_create_instance($this->instance); + + // Save an object with data in the field. + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $values; + $entity_type = 'test_entity'; + field_attach_insert($entity_type, $entity); + + // Verify the field data is present on load. + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $entity)); + $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], "Data are retrieved for the new bundle"); + + // Rename the bundle. This has to be initiated by the module so that its + // hook_fieldable_info() is consistent. + $new_bundle = 'test_bundle_' . strtolower($this->randomName()); + field_test_rename_bundle($this->instance['bundle'], $new_bundle); + + // Check that the instance definition has been updated. + $this->instance = field_info_instance($this->field_name, $new_bundle); + $this->assertIdentical($this->instance['bundle'], $new_bundle, "Bundle name has been updated in the instance."); + + // Verify the field data is present on load. + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $entity)); + $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], "Bundle name has been updated in the field storage"); + } + + function testFieldAttachDeleteBundle() { + // Create a new bundle. This has to be initiated by the module so that its + // hook_fieldable_info() is consistent. + $new_bundle = 'test_bundle_' . strtolower($this->randomName()); + field_test_create_bundle($new_bundle, $this->randomName()); + + // Add an instance to that bundle. + $this->instance['bundle'] = $new_bundle; + field_create_instance($this->instance); + + // Create a second field for the test bundle + $field_name = strtolower($this->randomName().'_field_name'); + $table = _field_sql_storage_tablename($field_name); + $revision_table = _field_sql_storage_revision_tablename($field_name); + $field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 1); + field_create_field($field); + $instance = array( + 'field_name' => $field_name, + 'bundle' => $this->instance['bundle'], + 'label' => $this->randomName().'_label', + 'description' => $this->randomName().'_description', + 'weight' => mt_rand(0, 127), + // test_field has no instance settings + 'widget' => array( + 'type' => 'test_field_widget', + 'settings' => array( + 'size' => mt_rand(0, 255)))); + field_create_instance($instance); + + // Save an object with data for both fields + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $values; + $entity->{$field_name} = array(0 => array('value' => 99)); + $entity_type = 'test_entity'; + field_attach_insert($entity_type, $entity); + + // Verify the fields are present on load + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $entity)); + $this->assertEqual(count($entity->{$this->field_name}), 4, "First field got loaded"); + $this->assertEqual(count($entity->{$field_name}), 1, "Second field got loaded"); + + // Delete the bundle. This has to be initiated by the module so that its + // hook_fieldable_info() is consistent. + field_test_delete_bundle($this->instance['bundle']); + + // Verify no data gets loaded + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + field_attach_load($entity_type, array(0 => $entity)); + $this->assertFalse(isset($entity->{$this->field_name}), "No data for first field"); + $this->assertFalse(isset($entity->{$field_name}), "No data for second field"); + + // Verify that the instances are gone + $this->assertFalse(field_read_instance($this->field_name, $this->instance['bundle']), "First field is deleted"); + $this->assertFalse(field_read_instance($field_name, $instance['bundle']), "Second field is deleted"); + } + + function testFieldAttachCache() { + // Create a revision + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(0, 127); + } + $entity->{$this->field_name} = $values; + + $noncached_type = 'test_entity'; + $cached_type = 'test_cacheable_entity'; + + // Non-cached type: + $cid = "field:$noncached_type:0:0"; + + // Confirm no initial cache entry + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no initial cache entry'); + + // Save, and confirm no cache entry + field_attach_insert($noncached_type, $entity); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on save'); + + // Load, and confirm no cache entry + field_attach_load($noncached_type, array(0 => $entity)); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on load'); + + // Cached type: + $cid = "field:$cached_type:0:0"; + + // Confirm no initial cache entry + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no initial cache entry'); + + // Save, and confirm no cache entry + field_attach_insert($cached_type, $entity); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on save'); + + // Load, and confirm cache entry + field_attach_load($cached_type, array(0 => $entity)); + $cache = cache_get($cid, 'cache_field'); + $this->assertEqual($cache->data[$this->field_name], $values, 'Cached: correct cache entry on load'); + + // Delete, and confirm no cache entry + field_attach_delete($cached_type, $entity); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on save'); + } + + // Verify that field_attach_validate() invokes the correct + // hook_field_validate. NOTE: This tests the FAPI-connected + // behavior of hook_field_validate. As discussed at + // http://groups.drupal.org/node/18019, field validation will + // eventually be disconnected from FAPI, at which point this + // function will have to be rewritten. + function testFieldAttachValidate() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Set up values to generate errors + $values = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = -1; + $values[$delta]['_error_element'] = 'field_error_'.$delta; + } + // Arrange for item 1 not to generate an error + $values[1]['value'] = 1; + $entity->{$this->field_name} = $values; + + field_attach_validate($entity_type, $entity, array()); + + $errors = form_get_errors(); + foreach ($values as $delta => $value) { + if ($value['value'] != 1) { + $this->assertTrue(isset($errors[$value['_error_element']]), "Error is set on {$value['_error_element']}: {$errors[$value['_error_element']]}"); + unset($errors[$value['_error_element']]); + } + else { + $this->assertFalse(isset($errors[$value['_error_element']]), "Error is not set on {$value['_error_element']}"); + } + } + $this->assertEqual(count($errors), 0, 'No extraneous form errors set'); + } + + // Validate that FAPI elements are generated. This could be much + // more thorough, but it does verify that the correct widgets show up. + function testFieldAttachForm() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + $form = $form_state = array(); + field_attach_form($entity_type, $entity, $form, $form_state); + + $this->assertEqual($form[$this->field_name]['#title'], $this->instance['label'], "Form title is {$this->instance['label']}"); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + // field_test_widget uses 'textfield' + $this->assertEqual($form[$this->field_name][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); + } + } + + function testFieldAttachSubmit() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Build the form. + $form = $form_state = array(); + field_attach_form($entity_type, $entity, $form, $form_state); + + // Simulate incoming values. + $values = array(); + $weights = array(); + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$delta]['value'] = mt_rand(1, 127); + // Assign random weight. + do { + $weight = mt_rand(0, $this->field['cardinality']); + } while (in_array($weight, $weights)); + $weights[$delta] = $weight; + $values[$delta]['_weight'] = $weight; + } + // Leave an empty value. 'field_test' fields are empty if empty(). + $values[1]['value'] = 0; + + $form_state['values'] = array($this->field_name => $values); + field_attach_submit($entity_type, $entity, $form, $form_state); + + asort($weights); + $expected_values = array(); + foreach ($weights as $key => $value) { + if ($key != 1) { + $expected_values[] = array('value' => $values[$key]['value']); + } + } + $this->assertIdentical($entity->{$this->field_name}, $expected_values, 'Submit filters empty values'); + } +} + +class FieldInfoTestCase extends DrupalWebTestCase { + + function getInfo() { + return array( + 'name' => t('Field info tests'), + 'description' => t("Get information about existing fields, instances and bundles."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + } + + function testFieldInfo() { + // Test that field_test module's fields, widgets, and formatters show up. + $field_test_info = field_test_field_info(); + $formatter_info = field_test_field_formatter_info(); + $widget_info = field_test_field_widget_info(); + + $info = field_info_field_types(); + foreach ($field_test_info as $t_key => $field_type) { + foreach ($field_type as $key => $val) { + $this->assertEqual($info[$t_key][$key], $val, t("Field type $t_key key $key is $val")); + } + $this->assertEqual($info[$t_key]['module'], 'field_test', t("Field type field_test module appears")); + } + + $info = field_info_formatter_types(); + foreach ($formatter_info as $f_key => $formatter) { + foreach ($formatter as $key => $val) { + $this->assertEqual($info[$f_key][$key], $val, t("Formatter type $f_key key $key is $val")); + } + $this->assertEqual($info[$f_key]['module'], 'field_test', t("Formatter type field_test module appears")); + } + + $info = field_info_widget_types(); + foreach ($widget_info as $w_key => $widget) { + foreach ($widget as $key => $val) { + $this->assertEqual($info[$w_key][$key], $val, t("Widget type $w_key key $key is $val")); + } + $this->assertEqual($info[$w_key]['module'], 'field_test', t("Widget type field_test module appears")); + } + + // Verify that no fields or instances exist + $fields = field_info_fields(); + $instances = field_info_instances(FIELD_TEST_BUNDLE); + $this->assertTrue(empty($fields), t('With no fields, info fields is empty.')); + $this->assertTrue(empty($instances), t('With no instances, info bundles is empty.')); + + // Create a field, verify it shows up. + $field = array( + 'field_name' => strtolower($this->randomName()), + 'type' => 'test_field', + ); + field_create_field($field); + $fields = field_info_fields(); + $this->assertEqual(count($fields), 1, t('One field exists')); + $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name')); + $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type')); + $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('info fields contains field module')); + $settings = array('test_field_setting' => 'dummy test string'); + foreach ($settings as $key => $val) { + $this->assertEqual($fields[$field['field_name']]['settings'][$key], $val, t("Field setting $key has correct default value $val")); + } + $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, t('info fields contains cardinality 1')); + $this->assertEqual($fields[$field['field_name']]['active'], 1, t('info fields contains active 1')); + + // Create an instance, verify that it shows up + $instance = array( + 'field_name' => $field['field_name'], + 'bundle' => FIELD_TEST_BUNDLE, + 'label' => $this->randomName(), + 'description' => $this->randomName(), + 'weight' => mt_rand(0, 127), + // test_field has no instance settings + 'widget' => array( + 'type' => 'test_field_widget', + 'settings' => array( + 'test_setting' => 999))); + field_create_instance($instance); + + $instances = field_info_instances($instance['bundle']); + $this->assertEqual(count($instances), 1, t('One instance shows up in info when attached to a bundle.')); + $this->assertTrue($instance < $instances[$instance['field_name']], t('Instance appears in info correctly')); + } + + // Test that the field_info settings convenience functions work + function testSettingsInfo() { + $info = field_test_field_info(); + foreach ($info as $type => $data) { + $this->assertIdentical(field_info_field_settings($type), $data['settings'], "field_info_field_settings returns {$type}'s field settings"); + $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], "field_info_field_settings returns {$type}'s field instance settings"); + } + + $info = field_test_field_widget_info(); + foreach ($info as $type => $data) { + $this->assertIdentical(field_info_widget_settings($type), $data['settings'], "field_info_widget_settings returns {$type}'s widget settings"); + } + + $info = field_test_field_formatter_info(); + foreach ($info as $type => $data) { + $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], "field_info_formatter_settings returns {$type}'s formatter settings"); + } + } +} + +class FieldFormTestCase extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Field form tests'), + 'description' => t("Test Field form handling."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + + $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content')); + $this->drupalLogin($web_user); + + $this->field_single = array('field_name' => strtolower($this->randomName().'_field_name'), 'type' => 'test_field'); + $this->field_multiple = array('field_name' => strtolower($this->randomName().'_field_name'), 'type' => 'test_field', 'cardinality' => 4); + $this->field_unlimited = array('field_name' => strtolower($this->randomName().'_field_name'), 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED); + + $this->instance = array( + 'bundle' => 'test_bundle', + 'label' => $this->randomName().'_label', + 'description' => $this->randomName().'_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + } + +// function testFieldFormSingle() { +// $this->field = $this->field_single; +// $this->field_name = $this->field['field_name']; +// $this->instance['field_name'] = $this->field_name; +// field_create_field($this->field); +// field_create_instance($this->instance); +// +// // Display creation form. +// $this->drupalGet('test-entity/add/test-bundle'); +// $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget is displayed'); +// $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); +// // TODO : check that the widget is populated with default value ? +// +// // Submit with invalid value (field-level validation). +// $edit = array($this->field_name . '[0][value]' => -1); +// $this->drupalPost(NULL, $edit, t('Save')); +// $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $this->instance['label'])), 'Field validation fails with invalid input.'); +// // TODO : check that the correct field is flagged for error. +// +// // Create an entity +// $value = mt_rand(0, 127); +// $edit = array($this->field_name . '[0][value]' => $value); +// $this->drupalPost(NULL, $edit, t('Save')); +// preg_match('|test-entity/(\d+)/edit|', $this->url, $match); +// $id = $match[1]; +// $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); +// $entity = field_test_entity_load($id); +// $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was saved'); +// +// // Display edit form. +// $this->drupalGet('test-entity/' . $id . '/edit'); +// $this->assertFieldByName($this->field_name . '[0][value]', $value, 'Widget is displayed with the correct default value'); +// $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); +// +// // Update the entity. +// $value = mt_rand(0, 127); +// $edit = array($this->field_name . '[0][value]' => $value); +// $this->drupalPost(NULL, $edit, t('Save')); +// $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); +// $entity = field_test_entity_load($id); +// $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was updated'); +// +// // Empty the field. +// $value = ''; +// $edit = array($this->field_name . '[0][value]' => $value); +// $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); +// $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); +// $entity = field_test_entity_load($id); +// $this->assertFalse(property_exists($entity, $this->field_name), 'Field was emptied'); +// +// } +// +// function testFieldFormSingleRequired() { +// $this->field = $this->field_single; +// $this->field_name = $this->field['field_name']; +// $this->instance['field_name'] = $this->field_name; +// $this->instance['required'] = TRUE; +// field_create_field($this->field); +// field_create_instance($this->instance); +// +// // Submit with missing required value. +// $edit = array(); +// $this->drupalPost('test-entity/add/test-bundle', $edit, t('Save')); +// $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); +// +// // Create an entity +// $value = mt_rand(0, 127); +// $edit = array($this->field_name . '[0][value]' => $value); +// $this->drupalPost(NULL, $edit, t('Save')); +// preg_match('|test-entity/(\d+)/edit|', $this->url, $match); +// $id = $match[1]; +// $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); +// $entity = field_test_entity_load($id); +// $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was saved'); +// +// // Edit with missing required value. +// $value = ''; +// $edit = array($this->field_name . '[0][value]' => $value); +// $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); +// $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); +// } + +// function testFieldFormMultiple() { +// $this->field = $this->field_multiple; +// $this->field_name = $this->field['field_name']; +// $this->instance['field_name'] = $this->field_name; +// field_create_field($this->field); +// field_create_instance($this->instance); +// } + + function testFieldFormUnlimited() { + $this->field = $this->field_unlimited; + $this->field_name = $this->field['field_name']; + $this->instance['field_name'] = $this->field_name; + field_create_field($this->field); + field_create_instance($this->instance); + + // Display creation form -> 1 widget. + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertFieldByName($this->field_name . '[0][value]', '', 'First widget is displayed'); + $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + + // Submit 'add more' button -> 2 widgets. + $this->drupalPost(NULL, array(), t('Add another item')); + $this->assertFieldByName($this->field_name . '[0][value]', '', 'First widget is displayed'); + $this->assertFieldByName($this->field_name . '[1][value]', '', 'Second widget is displayed'); + $this->assertNoField($this->field_name . '[2][value]', 'No extraneous widget is displayed'); + + // Submit 'add more' button with values filled in -> 3 widgets. + $values = array(mt_rand(0, 127), mt_rand(0, 127)); + $edit = array(); + foreach ($values as $key => $value) { + $edit["$this->field_name[$key][value]"] = $value; + } + $this->drupalPost(NULL, $edit, t('Add another item')); + $this->assertFieldByName($this->field_name . '[0][value]', $values[0], 'First widget is displayed and has the right value'); + $this->assertFieldByName($this->field_name . '[1][value]', $values[1], 'Second widget is displayed and has the right value'); + $this->assertFieldByName($this->field_name . '[2][value]', '', 'Third widget is displayed'); + $this->assertNoField($this->field_name . '[3][value]', 'No extraneous widget is displayed'); + + + // fill in values, change weights, press add more: check that the values and order are preserved, and the expected number of widgets is displayed + // submit: check that the entity is created with correct values and order + // display edit form: check that the expected number of widgets is displayed, with correct values + // change values, reorder, leave an empty value in the middle: check that the entity is updated with correct values + // re-submit: check that the field can be emptied. + + // Test with several multiple fields in a form + } + + // check with a multiple widget (implement a textfield with comma separated values) + + // check inaccessible fields are preserved on update + // check inaccessible fields get default value on insert (not implemented yet) + +} + +class FieldTestCase extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Field tests'), + 'description' => t("Create / read /update a field."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + } + + // TODO : test creation with + // - a full fledged $field structure, check that all the values are there + // - a minimal $field structure, check all default values are set + // defer actual $field comparison to a helper function, used for the two cases above + /** + * Test the creation of a field. + */ + function testCreateField() { + $field_definition = array( + 'field_name' => strtolower($this->randomName()), + 'type' => 'test_field', + ); + field_create_field($field_definition); + + $field = field_read_field($field_definition['field_name']); + + // Ensure that basic properties are preserved. + $this->assertEqual($field['field_name'], $field_definition['field_name'], t('The field name is properly saved.')); + $this->assertEqual($field['type'], $field_definition['type'], t('The field type is properly saved.')); + + // Ensure that cardinality defaults to 1. + $this->assertEqual($field['cardinality'], 1, t('Cardinality defaults to 1.')); + + // Ensure that default settings are present. + $info = field_info_field_types($field['type']); + $settings = $info['settings']; + $this->assertIdentical($settings, $field['settings'] , t('Default field settings have been written.')); + + // Check that a table has been created for the field. + $this->assertTrue(db_table_exists('field_data_' . $field_definition['field_name']), t('A table has been created for the field.')); + + // Guarantee that the name is unique. + try { + field_create_field($field_definition); + $this->fail(t('Cannot create two fields with the same name.')); + } catch (FieldException $e) { + $this->pass(t('Cannot create two fields with the same name.')); + } + + // Check that invalid field names are rejected. + $field_definition['field_name'] += '_#'; + try { + field_create_field($field_definition); + $this->fail(t('Cannot create a field with an invalid name.')); + } catch (FieldException $e) { + $this->pass(t('Cannot create a field with an invalid name.')); + } + + // TODO : other failures + } + + function testReadField() { + + } + + /** + * Test the deletion of a field. + */ + function testDeleteField() { + // TODO: Also test deletion of the data stored in the field ? + + // Create two fields (so we can test that only one is deleted). + $this->field = $this->drupalCreateField('test_field', 'test_field_name'); + $this->another_field = $this->drupalCreateField('test_field', 'another_test_field_name'); + + // Create instances for each. + $this->instance_definition = array( + 'field_name' => $this->field['field_name'], + 'bundle' => FIELD_TEST_BUNDLE, + 'widget' => array( + 'type' => 'test_field_widget', + ), + ); + field_create_instance($this->instance_definition); + $this->another_instance_definition = $this->instance_definition; + $this->another_instance_definition['field_name'] = $this->another_field['field_name']; + field_create_instance($this->another_instance_definition); + + // Test that the first field is not deleted, and then delete it. + $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE)); + $this->assertTrue(!empty($field) && empty($field['deleted']), t('A new field is not marked for deletion.')); + field_delete_field($this->field['field_name']); + + // Make sure that the field is marked as deleted when it is specifically + // loaded. + $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE)); + $this->assertTrue(!empty($field['deleted']), t('A deleted field is marked for deletion.')); + + // Make sure that this field's instance is marked as deleted when it is + // specifically loaded. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); + $this->assertTrue(!empty($instance['deleted']), t('An instance for a deleted field is marked for deletion.')); + + // Try to load the field normally and make sure it does not show up. + $field = field_read_field($this->field['field_name']); + $this->assertTrue(empty($field), t('A deleted field is not loaded by default.')); + + // Try to load the instance normally and make sure it does not show up. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $this->assertTrue(empty($instance), t('An instance for a deleted field is not loaded by default.')); + + // Make sure the other field (and its field instance) are not deleted. + $another_field = field_read_field($this->another_field['field_name']); + $this->assertTrue(!empty($another_field) && empty($another_field['deleted']), t('A non-deleted field is not marked for deletion.')); + $another_instance = field_read_instance($this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']); + $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('An instance of a non-deleted field is not marked for deletion.')); + } +} + +class FieldInstanceTestCase extends DrupalWebTestCase { + protected $field; + + function getInfo() { + return array( + 'name' => t('Field instance tests'), + 'description' => t("Create field entities by attaching fields to entities."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + + $this->field = $this->drupalCreateField('test_field'); + $this->instance_definition = array( + 'field_name' => $this->field['field_name'], + 'bundle' => FIELD_TEST_BUNDLE, + ); + } + + // TODO : test creation with + // - a full fledged $instance structure, check that all the values are there + // - a minimal $instance structure, check all default values are set + // defer actual $instance comparison to a helper function, used for the two cases above, + // and for testUpdateFieldInstance + function testCreateFieldInstance() { + field_create_instance($this->instance_definition); + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $field_type = field_info_field_types($this->field['type']); + + // Check that default values are set. + $this->assertIdentical($instance['required'], FALSE, t('Required defaults to false.')); + $this->assertIdentical($instance['label'], $this->instance_definition['field_name'], t('Label defaults to field name.')); + $this->assertIdentical($instance['description'], '', t('Description defaults to empty string.')); + + // Check that default instance settings are set. + $settings = array('test_instance_setting' => 'dummy test string'); + $this->assertIdentical($settings, $instance['settings'] , t('Default instance settings have been written.')); + // Check that the widget is the default one. + $this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], t('Default widget has been written.')); + // Check that default widget settings are set. + $settings = array('test_widget_setting' => 'dummy test string'); + $this->assertIdentical($settings, $instance['widget']['settings'] , t('Default widget settings have been written.')); + // Check that we have display info for 'full' build_mode. + $this->assertTrue(isset($instance['display']['full']), t('Display for "full" build_mode has been written.')); + // Check that the formatter is the default one. + $this->assertIdentical($instance['display']['full']['type'], $field_type['default_formatter'], t('Default formatter for "full" build_mode has been written.')); + // Check that the default formatter settings are set. + $info = field_info_formatter_types($instance['display']['full']['type']); + $settings = $info['settings']; + $this->assertIdentical($settings, $instance['display']['full']['settings'] , t('Default formatter settings for "full" build_mode have been written.')); + + // Guarantee that the field/bundle combination is unique. + try { + field_create_instance($this->instance_definition); + $this->fail(t('Cannot create two instances with the same field / bundle combination.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot create two instances with the same field / bundle combination.')); + } + + // Check that the specified field exists. + try { + $this->instance_definition['field_name'] = $this->randomName(); + field_create_instance($this->instance_definition); + $this->fail(t('Cannot create an instance of a non-existing field.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot create an instance of a non-existing field.')); + } + + // TODO: test other failures. + } + + function testReadFieldInstance() { + + } + + function testUpdateFieldInstance() { + field_create_instance($this->instance_definition); + $field_type = field_info_field_types($this->field['type']); + + // Check that basic changes are saved. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $instance['required'] = !$instance['required']; + $instance['weight']++; + $instance['label'] = $this->randomName(); + $instance['description'] = $this->randomName(); + $instance['settings']['test_instance_setting'] = $this->randomName(); + $instance['widget']['settings']['test_widget_setting'] =$this->randomName(); + $instance['display']['full']['settings']['test_formatter_setting'] = $this->randomName(); + field_update_instance($instance); + + $instance_new = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $this->assertEqual($instance['required'], $instance_new['required'], t('"required" change is saved')); + $this->assertEqual($instance['weight'], $instance_new['weight'], t('"weight" change is saved')); + $this->assertEqual($instance['label'], $instance_new['label'], t('"label" change is saved')); + $this->assertEqual($instance['description'], $instance_new['description'], t('"description" change is saved')); + $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], t('Widget setting change is saved')); + $this->assertEqual($instance['display']['full']['settings']['test_formatter_setting'], $instance_new['display']['full']['settings']['test_formatter_setting'], t('Formatter setting change is saved')); + + // Check that changing widget and formatter types updates the default settings. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $instance['widget']['type'] = 'test_field_widget_multiple'; + $instance['display']['full']['type'] = 'field_test_multiple'; + field_update_instance($instance); + + $instance_new = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $this->assertEqual($instance['widget']['type'], $instance_new['widget']['type'] , t('Widget type change is saved.')); + $settings = field_info_widget_settings($instance_new['widget']['type']); + $this->assertIdentical($settings, array_intersect_key($instance_new['widget']['settings'], $settings) , t('Widget type change updates default settings.')); + $this->assertEqual($instance['display']['full']['type'], $instance_new['display']['full']['type'] , t('Formatter type change is saved.')); + $info = field_info_formatter_types($instance_new['display']['full']['type']); + $settings = $info['settings']; + $this->assertIdentical($settings, array_intersect_key($instance_new['display']['full']['settings'], $settings) , t('Changing formatter type updates default settings.')); + + // Check that adding a new build mode is saved and gets default settings. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $instance['display']['teaser'] = array(); + field_update_instance($instance); + + $instance_new = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $this->assertTrue(isset($instance_new['display']['teaser']), t('Display for the new build_mode has been written.')); + $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], t('Default formatter for the new build_mode has been written.')); + $info = field_info_formatter_types($instance_new['display']['teaser']['type']); + $settings = $info['settings']; + $this->assertIdentical($settings, $instance_new['display']['teaser']['settings'] , t('Default formatter settings for the new build_mode have been written.')); + + // TODO: test failures. + } + + function testDeleteFieldInstance() { + // TODO: Test deletion of the data stored in the field also. + // Need to check that data for a 'deleted' field / instance doesn't get loaded + // Need to check data marked deleted is cleaned on cron (not implemented yet...) + + // Create two instances for the same field so we can test that only one + // is deleted. + field_create_instance($this->instance_definition); + $this->another_instance_definition = $this->instance_definition; + $this->another_instance_definition['bundle'] .= '_another_bundle'; + field_create_instance($this->another_instance_definition); + + // Test that the first instance is not deleted, and then delete it. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); + $this->assertTrue(!empty($instance) && empty($instance['deleted']), t('A new field instance is not marked for deletion.')); + field_delete_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + + // Make sure the instance is marked as deleted when the instance is + // specifically loaded. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); + $this->assertTrue(!empty($instance['deleted']), t('A deleted field instance is marked for deletion.')); + + // Try to load the instance normally and make sure it does not show up. + $instance = field_read_instance($this->instance_definition['field_name'], $this->instance_definition['bundle']); + $this->assertTrue(empty($instance), t('A deleted field instance is not loaded by default.')); + + // Make sure the other field instance is not deleted. + $another_instance = field_read_instance($this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']); + $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.')); + } +} === added directory 'modules/field/modules' === added directory 'modules/field/modules/field_sql_storage' === added file 'modules/field/modules/field_sql_storage/field_sql_storage.info' --- modules/field/modules/field_sql_storage/field_sql_storage.info 1970-01-01 00:00:00 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.info 2009-01-30 23:17:27 +0000 @@ -0,0 +1,8 @@ +; $Id$ +name = Field SQL storage +description = Stores field data in an SQL database. +package = Core - fields +core = 7.x +files[] = field_sql_storage.module +files[] = field_sql_storage.install +required = TRUE \ No newline at end of file === added file 'modules/field/modules/field_sql_storage/field_sql_storage.install' --- modules/field/modules/field_sql_storage/field_sql_storage.install 1970-01-01 00:00:00 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.install 2009-01-30 23:17:26 +0000 @@ -0,0 +1,53 @@ + array( + 'etid' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The unique id for this entity type', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'An entity type', + ), + ), + 'primary key' => array('etid'), + 'unique keys' => array('type' => array('type')), + ); + + // Dynamic (data) tables. + if (db_table_exists('field_config')) { + $fields = field_read_fields(); + drupal_load('module', 'field_sql_storage'); + $schema = array(); + foreach ($fields as $field) { + $schema += _field_sql_storage_schema($field); + } + } + return $schema; +} === added file 'modules/field/modules/field_sql_storage/field_sql_storage.module' --- modules/field/modules/field_sql_storage/field_sql_storage.module 1970-01-01 00:00:00 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 2009-01-30 23:17:27 +0000 @@ -0,0 +1,388 @@ +' . t('The Field SQL Storage module stores Field API data in the database. It is the default field storage module, but other field storage modules may be available in the contributions repository.') . '

'; + return $output; + } +} + +/** + * Generate a table name for a field data table. + * + * @param $name + * The name of the field + * @return + * A string containing the generated name for the database table + */ +function _field_sql_storage_tablename($name) { + return 'field_data_' . $name; +} + +/** + * Generate a table name for a field revision archive table. + * + * @param $name + * The name of the field + * @return + * A string containing the generated name for the database table + */ +function _field_sql_storage_revision_tablename($name) { + return 'field_data_revision_' . $name; +} + +/** + * Generate a column name for a field data table. + * + * @param $name + * The name of the field + * @param $column + * The name of the column + * @return + * A string containing a generated column name for a field data + * table that is unique among all other fields. + */ +function _field_sql_storage_columnname($name, $column) { + return $name . '_' . $column; +} + +/** + * Retrieve or assign an entity type id for an object type. + * + * @param $obj_type + * The object type, such as 'node' or 'user'. + * @return + * The entity type id. + * + * TODO: We need to decide on 'entity' or 'object'. + */ +function _field_sql_storage_etid($obj_type) { + $etid = variable_get('field_sql_storage_' . $obj_type . '_etid', NULL); + if (is_null($etid)) { + $etid = db_insert('field_config_entity_type')->fields(array('type' => $obj_type))->execute(); + variable_set('field_sql_storage_' . $obj_type . '_etid', $etid); + } + return $etid; +} + +/** + * Return the database schema for a field. This may contain one or + * more tables. Each table will contain the columns relevant for the + * specified field. Leave $field['columns'] empty to get only the + * base schema. + * + * @param $field + * The field structure for which to generate a database schema. + * @return + * One or more tables representing the schema for the field. + */ +function _field_sql_storage_schema($field) { + $current = array( + 'description' => 'Data storage for field '. $field['field_name'], + 'fields' => array( + 'etid' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity type id this data is attached to', + ), + 'bundle' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance', + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'A boolean indicating whether this data item has been deleted' + ), + 'entity_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity id this data is attached to', + ), + 'revision_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned', + ), + 'delta' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The sequence number for this data item, used for multi-value fields', + ), + ), + 'primary key' => array('etid', 'entity_id', 'deleted', 'delta'), + // TODO : index on 'bundle' + ); + + // Add field columns. + foreach ($field['columns'] as $column_name => $attributes) { + $current['fields'][_field_sql_storage_columnname($field['field_name'], $column_name)] = $attributes; + } + + // Construct the revision table. The primary key includes + // revision_id but not entity_id so that multiple revision loads can + // use the IN operator. + $revision = $current; + $revision['description'] = 'Revision archive storage for field '. $field['field_name']; + $revision['revision_id']['description'] = 'The entity revision id this data is attached to'; + $revision['primary key'] = array('etid', 'revision_id', 'deleted', 'delta'); + + return array( + _field_sql_storage_tablename($field['field_name']) => $current, + _field_sql_storage_revision_tablename($field['field_name']) => $revision, + ); +} + +function field_sql_storage_field_storage_create_field($field) { + $schema = _field_sql_storage_schema($field); + foreach ($schema as $name => $table) { + db_create_table($ret, $name, $table); + } +} + +function field_sql_storage_field_storage_delete_field($field_name) { + // Mark all data associated with the field for deletion. + $table = _field_sql_storage_tablename($field_name); + db_update($table) + ->fields(array('deleted' => 1)) + ->execute(); +} + +/** + * Load field data for a set of objects from the database. + * + * @param $obj_type + * The entity type of objects being loaded, such as 'node' or + * 'user'. + * @param $objects + * The array of objects for which to load data. + * @param $age + * FIELD_LOAD_CURRENT to load the most recent revision for all + * fields, or FIELD_LOAD_REVISION to load the version indicated by + * each object. + * @return + * An array of field data for the objects, keyed by entity id, field + * name, and item delta number. + */ +function field_sql_storage_field_storage_load($obj_type, $objects, $age) { + $etid = _field_sql_storage_etid($obj_type); + $load_current = $age == FIELD_LOAD_CURRENT; + + // Gather ids needed for each field. + $field_ids = array(); + $delta_count = array(); + foreach ($objects as $obj) { + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $obj); + foreach (field_info_instances($bundle) as $instance) { + $field_ids[$instance['field_name']][] = $load_current ? $id : $vid; + $delta_count[$id][$instance['field_name']] = 0; + } + } + + $additions = array(); + foreach ($field_ids as $field_name => $ids) { + $field = field_info_field($field_name); + $table = $load_current ? _field_sql_storage_tablename($field_name) : _field_sql_storage_revision_tablename($field_name); + + $results = db_select($table, 't') + ->fields('t') + ->condition('etid', $etid) + ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN') + ->condition('deleted', 0) + ->orderBy('delta') + ->execute(); + + foreach ($results as $row) { + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) { + $item = array(); + // For each column declared by the field, populate the item + // from the prefixed database column. + foreach ($field['columns'] as $column => $attributes) { + $item[$column] = $row->{_field_sql_storage_columnname($field_name, $column)}; + } + + // Add the item to the field values for the entity. + $additions[$row->entity_id][$field_name][] = $item; + $delta_count[$row->entity_id][$field_name]++; + } + } + } + return $additions; +} + +function field_sql_storage_field_storage_write($obj_type, $object, $update = FALSE) { + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); + $etid = _field_sql_storage_etid($obj_type); + + $instances = field_info_instances($bundle); + foreach ($instances as $instance) { + $field_name = $instance['field_name']; + $table_name = _field_sql_storage_tablename($field_name); + $revision_name = _field_sql_storage_revision_tablename($field_name); + $field = field_read_field($field_name); + + // Leave the field untouched if $object comes with no $field_name property. + // Empty the field if $object->$field_name is NULL or an empty array. + + // Function property_exists() is slower, so we catch the more frequent cases + // where it's an empty array with the faster isset(). + if (isset($object->$field_name) || property_exists($object, $field_name)) { + // Delete and insert, rather than update, in case a value was added. + if ($update) { + db_delete($table_name)->condition('etid', $etid)->condition('entity_id', $id)->execute(); + if (isset($vid)) { + db_delete($revision_name)->condition('etid', $etid)->condition('entity_id', $id)->condition('revision_id', $vid)->execute(); + } + } + + if ($object->$field_name) { + // Prepare the multi-insert query. + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); + foreach ($field['columns'] as $column => $attributes) { + $columns[] = _field_sql_storage_columnname($field_name, $column); + } + $query = db_insert($table_name)->fields($columns); + if (isset($vid)) { + $revision_query = db_insert($revision_name)->fields($columns); + } + + $delta_count = 0; + foreach ($object->$field_name as $delta => $item) { + $record = array( + 'etid' => $etid, + 'entity_id' => $id, + 'revision_id' => $vid, + 'bundle' => $bundle, + 'delta' => $delta, + ); + foreach ($field['columns'] as $column => $attributes) { + $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; + } + $query->values($record); + if (isset($vid)) { + $revision_query->values($record); + } + + if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { + break; + } + } + + // Execute the insert. + $query->execute(); + if (isset($vid)) { + $revision_query->execute(); + } + } + } + } +} + +/** + * Delete all field data for a single object. This function actually + * deletes the data from the database. + * + * @param $obj_type + * The entity type of the object being deleted, such as 'node' or + * 'user'. + * @param $object + * The object for which to delete field data. + */ +function field_sql_storage_field_storage_delete($obj_type, $object) { + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); + $etid = _field_sql_storage_etid($obj_type); + + $instances = field_info_instances($bundle); + foreach ($instances as $instance) { + $field_name = $instance['field_name']; + $table_name = _field_sql_storage_tablename($field_name); + $revision_name = _field_sql_storage_revision_tablename($field_name); + db_delete($table_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->execute(); + db_delete($revision_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->execute(); + } +} + +/** + * Delete field data for a single revision of a single object. + * Deleting the current (most recently written) revision is not + * allowed as has undefined results. This function actually deletes + * the data from the database. + * + * @param $obj_type + * The entity type of the object being deleted, such as 'node' or + * 'user'. + * @param $object + * The object for which to delete field data. + */ +function field_sql_storage_field_storage_delete_revision($obj_type, $object) { + list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); + $etid = _field_sql_storage_etid($obj_type); + + if (isset($vid)) { + $instances = field_info_instances($bundle); + foreach ($instances as $instance) { + $field_name = $instance['field_name']; + $revision_name = _field_sql_storage_revision_tablename($field_name); + db_delete($revision_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('revision_id', $vid) + ->execute(); + } + } +} + +function field_sql_storage_field_storage_delete_instance($field_name, $bundle) { + // Mark all data associated with the field for deletion. + $table_name = _field_sql_storage_tablename($field_name); + $revision_name = _field_sql_storage_revision_tablename($field_name); + db_update($table_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $bundle) + ->execute(); + db_update($revision_name) + ->fields(array('deleted' => 1)) + ->condition('bundle', $bundle) + ->execute(); +} + +function field_sql_storage_field_storage_rename_bundle($bundle_old, $bundle_new) { + $instances = field_info_instances($bundle_old); + foreach ($instances as $instance) { + $table_name = _field_sql_storage_tablename($instance['field_name']); + $revision_name = _field_sql_storage_revision_tablename($instance['field_name']); + db_update($table_name) + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle_old) + ->execute(); + db_update($revision_name) + ->fields(array('bundle' => $bundle_new)) + ->condition('bundle', $bundle_old) + ->execute(); + } +} \ No newline at end of file === added file 'modules/field/modules/field_sql_storage/field_sql_storage.test' --- modules/field/modules/field_sql_storage/field_sql_storage.test 1970-01-01 00:00:00 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.test 2009-01-30 23:17:27 +0000 @@ -0,0 +1,29 @@ + t('Field SQL Storage tests'), + 'description' => t("Test Field SQL Storage module."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field_sql_storage', 'field', 'field_test'); + } + + function testEntityTypeId() { + $t1 = _field_sql_storage_etid('t1'); + $t2 = _field_sql_storage_etid('t2'); + + $this->assertEqual($t1+1, $t2, 'Entity type ids are sequential'); + $this->assertIdentical(variable_get('field_sql_storage_t1_etid', NULL), $t1, 'First entity type variable is correct'); + $this->assertIdentical(variable_get('field_sql_storage_t2_etid', NULL), $t2, 'Second entity type variable is correct'); + $this->assertEqual(db_result(db_query("SELECT etid FROM {field_config_entity_type} WHERE type='t1'")), $t1, 'First entity type in database is correct'); + $this->assertEqual(db_result(db_query("SELECT etid FROM {field_config_entity_type} WHERE type='t2'")), $t2, 'Second entity type in database is correct'); + $this->assertEqual($t1, _field_sql_storage_etid('t1'), '_field_sql_storage_etid returns the same value for the first entity type'); + $this->assertEqual($t2, _field_sql_storage_etid('t2'), '_field_sql_storage_etid returns the same value for the second entity type'); + } +} === added directory 'modules/field/modules/list' === added file 'modules/field/modules/list/list.info' --- modules/field/modules/list/list.info 1970-01-01 00:00:00 +0000 +++ modules/field/modules/list/list.info 2009-01-30 23:17:26 +0000 @@ -0,0 +1,7 @@ +; $Id$ +name = List +description = Defines list field types. Use with Optionwidgets to create selection lists. +package = Core - fields +core = 7.x + +files[]=list.module === added file 'modules/field/modules/list/list.module' --- modules/field/modules/list/list.module 1970-01-01 00:00:00 +0000 +++ modules/field/modules/list/list.module 2009-01-30 23:17:27 +0000 @@ -0,0 +1,206 @@ + array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_list_key' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implementation of hook_field_info(). + */ +function list_field_info() { + return array( + 'list' => array( + 'label' => t('List'), + 'description' => t('This field stores numeric keys from key/value lists of allowed values where the key is a simple alias for the position of the value, i.e. 0|First option, 1|Second option, 2|Third option.'), + 'settings' => array('allowed_values' => '', 'allowed_values_function' => ''), + 'default_widget' => 'option_widgets_select', + 'default_formatter' => 'list_default', + ), + 'list_boolean' => array( + 'label' => t('Boolean'), + 'description' => t('This field stores simple on/off or yes/no options.'), + 'settings' => array('allowed_values' => '', 'allowed_values_function' => ''), + 'default_widget' => 'option_widgets_select', + 'default_formatter' => 'list_default', + ), + 'list_number' => array( + 'label' => t('List (numeric)'), + 'description' => t('This field stores keys from key/value lists of allowed numbers where the stored numeric key has significance and must be preserved, i.e. \'Lifetime in days\': 1|1 day, 7|1 week, 31|1 month.'), + 'settings' => array('allowed_values' => '', 'allowed_values_function' => ''), + 'default_widget' => 'option_widgets_select', + 'default_formatter' => 'list_default', + ), + 'list_text' => array( + 'label' => t('List (text)'), + 'description' => t('This field stores keys from key/value lists of allowed values where the stored key has significance and must be a varchar, i.e. \'US States\': IL|Illinois, IA|Iowa, IN|Indiana'), + 'settings' => array('allowed_values' => '', 'allowed_values_function' => ''), + 'default_widget' => 'option_widgets_select', + 'default_formatter' => 'list_default', + ), + ); +} + +/** + * Implementation of hook_field_schema(). + */ +function list_field_columns($field) { + switch ($field['type']) { + case 'list_text': + $columns = array( + 'value' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ); + break; + case 'list_number': + $columns = array( + 'value' => array( + 'type' => 'float', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + break; + default: + $columns = array( + 'value' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + break; + } + return $columns; +} + +/** + * Implementation of hook_field_validate(). + */ +function list_field_validate($obj_type, $object, $field, $instance, $items, $form) { + $allowed_values = list_allowed_values($field); + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if (!empty($item['value'])) { + if (count($allowed_values) && !array_key_exists($item['value'], $allowed_values)) { + form_set_error($error_element, t('%name: illegal value.', array('%name' => t($instance['label'])))); + } + } + } + } +} + +/** + * Implementation of hook_field_is_empty(). + */ +function list_field_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function list_field_formatter_info() { + return array( + 'list_default' => array( + 'label' => t('Default'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'list_key' => array( + 'label' => t('Key'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Theme function for 'default' list field formatter. + */ +function theme_field_formatter_list_default($element) { + $field = field_info_field($element['#field_name']); + if (($allowed_values = list_allowed_values($field)) && isset($allowed_values[$element['#item']['value']])) { + return $allowed_values[$element['#item']['value']]; + } + // If no match was found in allowed values, fall back to the key. + return $element['#item']['safe']; +} + +/** + * Theme function for 'key' list field formatter. + */ +function theme_field_formatter_list_key($element) { + return $element['#item']['safe']; +} + +/** + * Create an array of the allowed values for this field. + * + * Used by number and text fields, expects to find either + * a function that will return the correct value, or a string + * with keys and labels separated with '|' and with each + * new value on its own line. + */ +// TODO Rework this to create a method of selecting plugable allowed values lists. +function list_allowed_values($field) { + static $allowed_values; + + if (isset($allowed_values[$field['field_name']])) { + return $allowed_values[$field['field_name']]; + } + + $allowed_values[$field['field_name']] = array(); + + if (isset($field['settings']['allowed_values_function'])) { + $function = $field['settings']['allowed_values_function']; + if (drupal_function_exists($function)) { + $allowed_values[$field['field_name']] = $function($field); + } + } + + if (empty($allowed_values[$field['field_name']]) && isset($field['settings']['allowed_values'])) { + $list = explode("\n", $field['settings']['allowed_values']); + $list = array_map('trim', $list); + $list = array_filter($list, 'strlen'); + foreach ($list as $opt) { + // Sanitize the user input with a permissive filter. + $opt = field_filter_xss($opt); + if (strpos($opt, '|') !== FALSE) { + list($key, $value) = explode('|', $opt); + $allowed_values[$field['field_name']][$key] = (isset($value) && $value !=='') ? $value : $key; + } + else { + $allowed_values[$field['field_name']][$opt] = $opt; + } + } + } + return $allowed_values[$field['field_name']]; +} === added directory 'modules/field/modules/number' === added file 'modules/field/modules/number/number.info' --- modules/field/modules/number/number.info 1970-01-01 00:00:00 +0000 +++ modules/field/modules/number/number.info 2009-01-30 23:17:27 +0000 @@ -0,0 +1,7 @@ +; $Id$ +name = Number +description = Defines numeric field types. +package = Core - fields +core = 7.x + +files[]=number.module === added file 'modules/field/modules/number/number.module' --- modules/field/modules/number/number.module 1970-01-01 00:00:00 +0000 +++ modules/field/modules/number/number.module 2009-01-30 23:17:27 +0000 @@ -0,0 +1,426 @@ + array('arguments' => array('element' => NULL)), + 'field_formatter_number_integer' => array('arguments' => array('element' => NULL), 'function' => 'theme_field_formatter_number'), + 'field_formatter_number_decimal' => array('arguments' => array('element' => NULL), 'function' => 'theme_field_formatter_number'), + 'field_formatter_number_unformatted' => array('arguments' => array('element' => NULL)), + ); +} + +/** + * Implementation of hook_field_info(). + */ +function number_field_info() { + return array( + 'number_integer' => array( + 'label' => t('Integer'), + 'description' => t('This field stores a number in the database as an integer.'), + 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), + 'default_widget' => 'number', + 'default_formatter' => 'number_integer', + ), + 'number_decimal' => array( + 'label' => t('Decimal'), + 'description' => t('This field stores a number in the database in a fixed decimal format.'), + 'settings' => array('precision' => 10, 'scale' => 2, 'decimal' => '.'), + 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), + 'default_widget' => 'number', + 'default_formatter' => 'number_integer', + ), + 'number_float' => array( + 'label' => t('Float'), + 'description' => t('This field stores a number in the database in a floating point format.'), + 'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''), + 'default_widget' => 'number', + 'default_formatter' => 'number_integer', + ), + ); +} + +function number_field_columns($field) { + switch ($field['type']) { + case 'number_integer' : + $colums = array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE + ), + ); + break; + + case 'number_float' : + $colums = array( + 'value' => array( + 'type' => 'float', + 'not null' => FALSE + ), + ); + break; + + case 'number_decimal' : + $colums = array( + 'value' => array( + 'type' => 'numeric', + 'precision' => $field['settings']['precision'], + 'scale' => $field['settings']['scale'], + 'not null' => FALSE + ), + ); + break; + } + return $colums; +} + +/** + * Implementation of hook_field_validate(). + */ +function number_field_validate($obj_type, $node, $field, $instance, &$items, $form) { + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if ($item['value'] != '') { + if (is_numeric($instance['settings']['min']) && $item['value'] < $instance['settings']['min']) { + form_set_error($error_element, t('%name: the value may be no smaller than %min.', array('%name' => t($instance['label']), '%min' => $instance['settings']['min']))); + } + if (is_numeric($instance['settings']['max']) && $item['value'] > $instance['settings']['max']) { + form_set_error($error_element, t('%name: the value may be no larger than %max.', array('%name' => t($instance['label']), '%max' => $instance['settings']['max']))); + } + } + } + } +} + +/** + * Implementation of hook_content_is_empty(). + */ +function number_field_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function number_field_formatter_info() { + return array( + 'number_integer' => array( + 'label' => t('default'), + 'field types' => array('number_integer'), + 'settings' => array( + 'thousand_separator' => ' ', + 'decimal_separator' => '.', + 'scale' => 0, + 'prefix_suffix' => TRUE, + ), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'number_decimal' => array( + 'label' => t('default'), + 'field types' => array('number_decimal', 'number_float'), + 'settings' => array( + 'thousand_separator' => ' ', + 'decimal_separator' => '.', + 'scale' => 2, + 'prefix_suffix' => TRUE, + ), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'number_unformatted' => array( + 'label' => t('unformatted'), + 'field types' => array('number_integer', 'number_decimal', 'number_float'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Theme function for 'unformatted' number field formatter. + */ +function theme_field_formatter_number_unformatted($element) { + return $element['#item']['value']; +} + +/** + * Proxy theme function for number field formatters. + */ +function theme_field_formatter_number($element) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $value = $element['#item']['value']; + $settings = $element['#settings']; + $formatter_type = $element['#formatter']; + + if (empty($value) && $value !== '0') { + return ''; + } + + $output = number_format($value, $settings['scale'], $settings['decimal_separator'], $settings['thousand_separator']); + + if ($settings['prefix_suffix']) { + $prefixes = isset($instance['settings']['prefix']) ? explode('|', check_plain($instance['settings']['prefix'])) : array(0 => ''); + $suffixes = isset($instance['settings']['suffix']) ? explode('|', check_plain($instance['settings']['suffix'])) : array(0 => ''); + $prefix = (count($prefixes) > 1) ? format_plural($value, $prefixes[0], $prefixes[1]) : $prefixes[0]; + $suffix = (count($suffixes) > 1) ? format_plural($value, $suffixes[0], $suffixes[1]) : $suffixes[0]; + $output = $prefix . $output . $suffix; + } + + return $output; +} + +/** + * Implementation of hook_field_widget_info(). + * + * Here we indicate that the content module will handle + * the default value and multiple values for these widgets. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function number_field_widget_info() { + return array( + 'number' => array( + 'label' => t('Text field'), + 'field types' => array('number_integer', 'number_decimal', 'number_float'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Includes a regex to check for valid values as an additional parameter + * the validator can use. The regex can be overridden if necessary. + */ +function number_elements() { + return array( + 'number' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('number_process'), + ), + ); +} + +/** + * Implementation of hook_field_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'] holds the form values. + * @param $field + * The field structure. + * @param $instance + * the field instance array + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function number_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = 0) { + $element = array( + '#type' => $instance['widget']['type'], + '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $field and $instance arrays are in $form['#fields'][$element['#field_name']]. + */ +function number_process($element, $edit, $form_state, $form) { + $field_name = $element['#field_name']; + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $field_key = $element['#columns'][0]; + + $value = isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : ''; + if ($field['type'] == 'number_decimal') { + $value = str_replace('.', $field['settings']['decimal'], $value); + } + + $element[$field_key] = array( + '#type' => 'textfield', + '#default_value' => $value, + // Need to allow a slightly larger size that the field length to allow + // for some configurations where all characters won't fit in input field. + '#size' => $field['type'] == 'number_decimal' ? $field['settings']['precision'] + 2 : 12, + '#maxlength' => $field['type'] == 'number_decimal' ? $field['settings']['precision'] : 10, + '#attributes' => array('class' => 'number'), + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#bundle' => $element['#bundle'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + if (!empty($instance['settings']['prefix'])) { + $prefixes = explode('|', $instance['settings']['prefix']); + $element[$field_key]['#field_prefix'] = array_pop($prefixes); + } + if (!empty($instance['settings']['suffix'])) { + $suffixes = explode('|', $instance['settings']['suffix']); + $element[$field_key]['#field_suffix'] = array_pop($suffixes); + } + + // Make sure we don't wipe out element validation added elsewhere. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + switch ($field['type']) { + case 'number_float': + $element['#element_validate'][] = 'number_float_validate'; + break; + case 'number_integer': + $element['#element_validate'][] = 'number_integer_validate'; + break; + case 'number_decimal': + $element['#element_validate'][] = 'number_decimal_validate'; + break; + } + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + + return $element; +} + +/** + * FAPI validation of an individual float element. + */ +function number_float_validate($element, &$form_state) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $start = $value; + $value = preg_replace('@[^-0-9\.]@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers and decimals are allowed in %field.', array('%field' => t($instance['label'])))); + } + else { + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI validation of an individual integer element. + */ +function number_integer_validate($element, &$form_state) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $start = $value; + $value = preg_replace('@[^-0-9]@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers are allowed in %field.', array('%field' => t($instance['label'])))); + } + else { + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI validation of an individual decimal element. + */ +function number_decimal_validate($element, &$form_state) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $start = $value; + $value = preg_replace('@[^-0-9\\'. $field['settings']['decimal'] .']@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers and the decimal character (%decimal) are allowed in %field.', array('%decimal' => $field['settings']['decimal'], '%field' => t($instance['label'])))); + } + else { + $value = str_replace($field['settings']['decimal'], '.', $value); + $value = round($value, $field['settings']['scale']); + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI theme for an individual number element. + * + * The textfield is already rendered by the textfield + * theme and the HTML output lives in $element['#children']. + * Override this theme to make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_number($element) { + return $element['#children']; +} \ No newline at end of file === added directory 'modules/field/modules/option_widgets' === added file 'modules/field/modules/option_widgets/option_widgets.info' --- modules/field/modules/option_widgets/option_widgets.info 1970-01-01 00:00:00 +0000 +++ modules/field/modules/option_widgets/option_widgets.info 2009-01-30 23:17:27 +0000 @@ -0,0 +1,6 @@ +; $Id$ +name = Optionwidgets +description = Defines selection, check box and radio button widgets for text and numeric fields. +package = Core - fields +core = 7.x +files[]=option_widgets.module === added file 'modules/field/modules/option_widgets/option_widgets.module' --- modules/field/modules/option_widgets/option_widgets.module 1970-01-01 00:00:00 +0000 +++ modules/field/modules/option_widgets/option_widgets.module 2009-01-30 23:17:26 +0000 @@ -0,0 +1,452 @@ +'. t('Create a list of options as a list in Allowed values list or as an array in PHP code. These values will be the same for %field in all field types.', array('%field' => $label)) .'

'; + + if ($widget_type == 'option_widgets_onoff') { + $output .= '

'. t("For a 'single on/off checkbox' widget, define the 'off' value first, then the 'on' value in the Allowed values section. Note that the checkbox will be labeled with the label of the 'on' value.") .'

'; + } + elseif ($widget_type == 'option_widgets_buttons') { + $output .= '

'. t("The 'checkboxes/radio buttons' widget will display checkboxes if the cardinality option is selected for this field, otherwise radios will be displayed.") .'

'; + } + + if (in_array($field_type, array('text', 'number_integer', 'number_float', 'number_decimal')) + && in_array($widget_type, array('option_widgets_onoff', 'option_widgets_buttons', 'option_widgets_select'))) { + $form['field']['allowed_values_fieldset']['#collapsed'] = FALSE; + $form['field']['allowed_values_fieldset']['#description'] = $output; + + // If no 'allowed values' were set yet, add a remainder in the messages area. + if (empty($form_state['post']) + && empty($form['field']['allowed_values_fieldset']['allowed_values']['#default_value']) + && empty($form['field']['allowed_values_fieldset']['advanced_options']['allowed_values_php']['#default_value'])) { + drupal_set_message(t("You need to specify the 'allowed values' for this field."), 'warning'); + } + } + } +} + +/** + * Implementation of hook_theme(). + */ +function option_widgets_theme() { + return array( + 'option_widgets_select' => array( + 'arguments' => array('element' => NULL), + ), + 'option_widgets_buttons' => array( + 'arguments' => array('element' => NULL), + ), + 'option_widgets_onoff' => array( + 'arguments' => array('element' => NULL), + ), + 'option_widgets_none' => array( + 'arguments' => array('widget_type' => NULL, 'field_name' => NULL, 'node_type' => NULL), + ), + ); +} + +/** + * Implementation of hook_field_widget_info(). + * + * We need custom handling of multiple values because we need + * to combine them into a options list rather than display + * cardinality elements. We will use the field module's default + * handling for default values. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function option_widgets_field_widget_info() { + + return array( + 'option_widgets_select' => array( + 'label' => t('Select list'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'option_widgets_buttons' => array( + 'label' => t('Check boxes/radio buttons'), + 'field types' => array('list', 'list_boolean', 'list_text', 'list_number'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'option_widgets_onoff' => array( + 'label' => t('Single on/off checkbox'), + 'field types' => array('list_boolean'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + */ +function option_widgets_elements() { + return array( + 'option_widgets_select' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('option_widgets_select_process'), + ), + 'option_widgets_buttons' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('option_widgets_buttons_process'), + ), + 'option_widgets_onoff' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('option_widgets_onoff_process'), + ), + ); +} + +/** + * Implementation of hook_field_widget(). + */ +function option_widgets_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = NULL) { + $element = array( + '#type' => $instance['widget']['type'], + '#default_value' => !empty($items) ? $items : array(), + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $field and $instance arrays are in $form['#fields'][$element['#field_name']]. + */ +function option_widgets_buttons_process($element, $edit, &$form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = option_widgets_data2form($element, $element['#default_value'], $field); + } + $options = option_widgets_options($field, $instance); + $multiple = isset($element['#multiple']) ? $element['#multiple'] : $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; + + $value = array(); + foreach ($element['#value'][$field_key] as $key) { + // Multiple (checkboxes) need the default value in the form of an array. + if ($multiple) { + $value[$key] = 1; + } + // Non-multiple (radios) need single default value. + else { + $value = $key; + break; + } + } + + $element[$field_key] = array( + '#type' => $multiple ? 'checkboxes' : 'radios', + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => isset($element['#required']) ? $element['#required'] : $instance['required'], + '#multiple' => $multiple, + '#options' => $options, + '#default_value' => $value, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'option_widgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#fields'][$element['#field_name']] = $form['#fields'][$element['#field_name']]; + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $field and $instance arrays are in $form['#fields'][$element['#field_name']]. + */ +function option_widgets_select_process($element, $edit, &$form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = option_widgets_data2form($element, $element['#default_value'], $field); + } + + $options = option_widgets_options($field, $instance); + $element[$field_key] = array( + '#type' => 'select', + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => isset($element['#required']) ? $element['#required'] : $instance['required'], + '#multiple' => isset($element['#multiple']) ? $element['#multiple'] : $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED, + '#options' => $options, + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'option_widgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#fields'][$element['#field_name']] = $form['#fields'][$element['#field_name']]; + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + */ +function option_widgets_onoff_process($element, $edit, &$form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = option_widgets_data2form($element, $element['#default_value'], $field); + } + $options = option_widgets_options($field, $instance); + $keys = array_keys($options); + $on_value = (!empty($keys) && isset($keys[1])) ? $keys[1] : NULL; + $element[$field_key] = array( + '#type' => 'checkbox', + '#title' => isset($options[$on_value]) ? $options[$on_value] : '', + '#description' => $element['#description'], + '#default_value' => isset($element['#value'][$field_key][0]) ? $element['#value'][$field_key][0] == $on_value : FALSE, + '#return_value' => $on_value, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'option_widgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#fields'][$element['#field_name']] = $form['#fields'][$element['#field_name']]; + return $element; +} + +/** + * FAPI function to validate option_widgets element. + */ +function option_widgets_validate($element, &$form_state) { + // Transpose selections from field => delta to delta => field, + // turning cardinality selected options into cardinality parent elements. + // Immediate parent is the delta, need to get back to parent's parent + // to create cardinality elements. + $field = $form_state['#fields'][$element['#field_name']]['field']; + $items = option_widgets_form2data($element, $field); + form_set_value($element, $items, $form_state); + + // Check we don't exceed the allowed number of values. + if ($field['cardinality'] >= 2) { + // Filter out 'none' value (if present, will always be in key 0) + $field_key = $element['#columns'][0]; + if (isset($items[0][$field_key]) && $items[0][$field_key] === '') { + unset($items[0]); + } + if (count($items) > $field['cardinality']) { + $field_key = $element['#columns'][0]; + form_error($element[$field_key], t('%name: this field cannot hold more that @count values.', array('%name' => t($field['widget']['label']), '@count' => $field['cardinality']))); + } + } +} + +/** + * Helper function to transpose the values as stored in the database + * to the format the widget needs. Can be called anywhere this + * transformation is needed. + */ +function option_widgets_data2form($element, $items, $field) { + $field_key = $element['#columns'][0]; + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $options = option_widgets_options($field, $instance); + + $items_transposed = field_transpose_array_rows_cols($items); + $values = (isset($items_transposed[$field_key]) && is_array($items_transposed[$field_key])) ? $items_transposed[$field_key] : array(); + $keys = array(); + foreach ($values as $value) { + $key = array_search($value, array_keys($options)); + if (isset($key)) { + $keys[] = $value; + } + } + if ($field['cardinality'] || $element['#type'] == 'option_widgets_onoff') { + return array($field_key => $keys); + } + else { + return !empty($keys) ? array($field_key => $value) : array(); + } +} + +/** + * Helper function to transpose the values returned by submitting the widget + * to the format to be stored in the field. Can be called anywhere this + * transformation is needed. + */ +function option_widgets_form2data($element, $field) { + $field_key = $element['#columns'][0]; + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + $items = (array) $element[$field_key]['#value']; + $options = option_widgets_options($field, $instance); + + $values = array_values($items); + + if ($element['#type'] == 'option_widgets_onoff' && ($values[0] === 0)) { + $keys = array_keys($options); + $values = array(array_key_exists(0, $keys) ? $keys[0] : NULL); + } + + if (empty($values)) { + $values[] = NULL; + } + $result = field_transpose_array_rows_cols(array($field_key => $values)); + return $result; +} + +/** + * Helper function for finding the allowed values list for a field. + * + * See if there is a module hook for the option values. + * Otherwise, try list_allowed_values() for an options list. + */ +function option_widgets_options($field, $instance) { + $function = $field['module'] .'_allowed_values'; + $options = function_exists($function) ? $function($field) : (array) list_allowed_values($field); + // Add an empty choice for : + // - non required radios + // - non required selects + if (!$instance['required']) { + if ((in_array($instance['widget']['type'], array('option_widgets_buttons', 'node_reference_buttons', 'user_reference_buttons')) && !$field['cardinality']) + || (in_array($instance['widget']['type'], array('option_widgets_select', 'node_reference_select', 'user_reference_select')))) { + $options = array('' => theme('option_widgets_none', $instance)) + $options; + } + } + return $options; +} + +/** + * Theme the label for the empty value for options that are not required. + * The default theme will display N/A for a radio list and blank for a select. + */ +function theme_option_widgets_none($instance) { + switch ($instance['widget']['type']) { + case 'option_widgets_buttons': + case 'node_reference_buttons': + case 'user_reference_buttons': + return t('N/A'); + case 'option_widgets_select': + case 'node_reference_select': + case 'user_reference_select': + return t('- None -'); + default : + return ''; + } +} + +/** + * FAPI themes for option_widgets. + * + * The select, checkboxes or radios are already rendered by the + * select, checkboxes, or radios themes and the HTML output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_option_widgets_select($element) { + return $element['#children']; +} + +function theme_option_widgets_onoff($element) { + return $element['#children']; +} + +function theme_option_widgets_buttons($element) { + return $element['#children']; +} \ No newline at end of file === added directory 'modules/field/modules/text' === added file 'modules/field/modules/text/text.info' --- modules/field/modules/text/text.info 1970-01-01 00:00:00 +0000 +++ modules/field/modules/text/text.info 2009-01-30 23:17:27 +0000 @@ -0,0 +1,7 @@ +; $Id: text.info,v 1.9 2008/04/23 18:02:31 dww Exp $ +name = Text +description = Defines simple text field types. +package = Core - fields +core = 7.x + +files[]=text.module === added file 'modules/field/modules/text/text.module' --- modules/field/modules/text/text.module 1970-01-01 00:00:00 +0000 +++ modules/field/modules/text/text.module 2009-01-30 23:17:28 +0000 @@ -0,0 +1,402 @@ + array( + 'arguments' => array('element' => NULL), + ), + 'text_textfield' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_text_default' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_text_plain' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_text_trimmed' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implementation of hook_field_info(). + */ +function text_field_info() { + return array( + 'text' => array( + 'label' => t('Text'), + 'description' => t('This field stores varchar text in the database.'), + 'settings' => array('max_length' => 255), + 'instance_settings' => array('text_processing' => 0), + 'default_widget' => 'text_textfield', + 'default_formatter' => 'text_default', + ), + // TODO : not a good name... + 'textarea' => array( + 'label' => t('Textarea'), + 'description' => t('This field stores long text in the database.'), + 'instance_settings' => array('text_processing' => 0), + 'default_widget' => 'text_textarea', + 'default_formatter' => 'text_default', + ), + ); +} + +/** + * Implementation of hook_field_schema(). + */ +function text_field_columns($field) { + if ($field['type'] == 'textarea') { + $columns = array( + 'value' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + ); + } + else { + $columns = array( + 'value' => array( + 'type' => 'varchar', + 'length' => $field['settings']['max_length'], + 'not null' => FALSE, + ), + ); + } + $columns += array( + 'format' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + return $columns; +} + +/** + * Implementation of hook_field_validate(). + */ +function text_field_validate($obj_type, $object, $field, $instance, $items, $form) { + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if (!empty($item['value'])) { + if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) { + form_set_error($error_element, t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']))); + } + } + } + } +} + +function text_field_sanitize($obj_type, $object, $field, $instance, &$items) { + global $language; + foreach ($items as $delta => $item) { + // TODO D7 : this code is really node-related. + if (!empty($instance['settings']['text_processing'])) { + $check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW); + $text = isset($item['value']) ? check_markup($item['value'], $item['format'], isset($object->language) ? $object->language : $language, $check) : ''; + } + else { + $text = check_plain($item['value']); + } + $items[$delta]['safe'] = $text; + } +} + +/** + * Implementation of hook_field_is_empty(). + */ +function text_field_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function text_field_formatter_info() { + return array( + 'text_default' => array( + 'label' => t('Default'), + 'field types' => array('text', 'textarea'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'text_plain' => array( + 'label' => t('Plain text'), + 'field types' => array('text', 'textarea'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'text_trimmed' => array( + 'label' => t('Trimmed'), + 'field types' => array('text', 'textarea'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Theme function for 'default' text field formatter. + */ +function theme_field_formatter_text_default($element) { + return $element['#item']['safe']; +} + +/** + * Theme function for 'plain' text field formatter. + */ +function theme_field_formatter_text_plain($element) { + return strip_tags($element['#item']['safe']); +} + +/** + * Theme function for 'trimmed' text field formatter. + */ +function theme_field_formatter_text_trimmed($element) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + return $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL; +} + +/** + * Implementation of hook_field_widget_info(). + * + * Here we indicate that the field module will handle + * the default value and multiple values for these widgets. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function text_field_widget_info() { + return array( + 'text_textfield' => array( + 'label' => t('Text field'), + 'field types' => array('text'), + 'settings' => array('size' => 60), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'text_textarea' => array( + 'label' => t('Text area (multiple rows)'), + 'field types' => array('textarea'), + 'settings' => array('rows' => 5), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Autocomplete_path is not used by text_field_widget but other + * widgets can use it (see nodereference and userreference). + */ +function text_elements() { + return array( + 'text_textfield' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('text_textfield_process'), + '#autocomplete_path' => FALSE, + ), + 'text_textarea' => array( + '#input' => TRUE, + '#columns' => array('value', 'format'), '#delta' => 0, + '#process' => array('text_textarea_process'), + '#filter_value' => FILTER_FORMAT_DEFAULT, + ), + ); +} + +/** + * Implementation of hook_field_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_field_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * field module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the field module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * The field structure. + * @param $instance + * the field instance array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function text_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = 0) { + $element = array( + '#type' => $instance['widget']['type'], + '#default_value' => isset($items[$delta]) ? $items[$delta] : '', + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $field and $instance arrays are in $form['#fields'][$element['#field_name']]. + * + * TODO: For widgets to be actual FAPI 'elements', reusable outside of a + * 'field' context, they shoudn't rely on $field and $instance. The bits of + * information needed to adjust the behavior of the 'element' should be + * extracted in hook_field_widget() above. + */ +function text_textfield_process($element, $edit, $form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $field_key = $element['#columns'][0]; + $delta = $element['#delta']; + + $element[$field_key] = array( + '#type' => 'textfield', + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + '#autocomplete_path' => $element['#autocomplete_path'], + '#size' => $instance['widget']['settings']['size'], + '#attributes' => array('class' => 'text'), + // The following values were set by the field module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#bundle' => $element['#bundle'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + $element[$field_key]['#maxlength'] = !empty($field['settings']['max_length']) ? $field['settings']['max_length'] : NULL; + + if (!empty($instance['settings']['text_processing'])) { + $filter_key = $element['#columns'][1]; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $parents = array_merge($element['#parents'] , array($filter_key)); + $element[$filter_key] = filter_form($format, 1, $parents); + } + + // Used so that hook_field('validate') knows where to flag an error. + // TODO: rework that. See http://groups.drupal.org/node/18019. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $field and $instance arrays are in $form['#fields'][$element['#field_name']]. + */ +function text_textarea_process($element, $edit, $form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $field_key = $element['#columns'][0]; + $delta = $element['#delta']; + $element[$field_key] = array( + '#type' => 'textarea', + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + '#rows' => $instance['widget']['settings']['rows'], + '#weight' => 0, + // The following values were set by the field module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#bundle' => $element['#bundle'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + if (!empty($instance['settings']['text_processing'])) { + $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format'; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $parents = array_merge($element['#parents'] , array($filter_key)); + $element[$filter_key] = filter_form($format, 1, $parents); + } + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + return $element; +} + +/** + * FAPI theme for an individual text elements. + * + * The textfield or textarea is already rendered by the + * textfield or textarea themes and the html output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_text_textfield($element) { + return $element['#children']; +} + +function theme_text_textarea($element) { + return $element['#children']; +} \ No newline at end of file === added file 'modules/field/modules/text/text.test' --- modules/field/modules/text/text.test 1970-01-01 00:00:00 +0000 +++ modules/field/modules/text/text.test 2009-01-30 23:17:27 +0000 @@ -0,0 +1,39 @@ + t('Text Field'), + 'description' => t("Test the creation of text fields."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('field', 'text', 'field_test'); + } + + // Test widgets. + + /** + * Test textfield widget. + */ + function testTextfieldWidget() { + // Create a field + $field = $this->drupalCreateField('text'); + $this->instance = $this->drupalCreateFieldInstance($field['field_name'], 'text_textfield', 'text_default', FIELD_TEST_BUNDLE); + + } + + /** + * Test textarea widget. + */ + + // Test formatters. + /** + * + */ +} === added file 'modules/field/sample_code.php' --- modules/field/sample_code.php 1970-01-01 00:00:00 +0000 +++ modules/field/sample_code.php 2009-01-30 23:17:27 +0000 @@ -0,0 +1,103 @@ + 'field_single', + 'type' => 'text', +); +field_create_field($field); + +$instance = array( + 'field_name' => 'field_single', + 'bundle' => 'article', + 'label' => 'Single', + 'widget' => array( + 'type' => 'text_textfield', + ), + 'display' => array( + 'full' => array( + 'label' => 'above', + 'type' => 'text_default', + 'exclude' => 0, + ), + ), +); +field_create_instance($instance); + +$instance['bundle'] = 'user'; +field_create_instance($instance); + + +$field = array( + 'field_name' => 'field_multiple', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'type' => 'text', +); +field_create_field($field); + +$instance = array( + 'field_name' => 'field_multiple', + 'bundle' => 'article', + 'label' => 'Multiple', + 'widget' => array( + 'type' => 'text_textfield', + ), + 'display' => array( + 'full' => array( + 'label' => 'above', + 'type' => 'text_default', + 'exclude' => 0, + ), + ), +); +field_create_instance($instance); + +$instance['bundle'] = 'user'; +field_create_instance($instance); + + +// Number +$field = array( + 'field_name' => 'field_integer', + 'type' => 'number_integer', +); +field_create_field($field); +$instance = array( + 'field_name' => 'field_integer', + 'bundle' => 'article', + 'label' => 'Integer', + 'widget' => array( + 'type' => 'number', + ), + 'display' => array( + 'full' => array( + 'label' => 'above', + 'type' => 'number_integer', + 'exclude' => 0, + ), + ), +); +field_create_instance($instance); + +$field = array( + 'field_name' => 'field_decimal', + 'type' => 'number_decimal', +); +field_create_field($field); +$instance = array( + 'field_name' => 'field_decimal', + 'bundle' => 'article', + 'label' => 'Decimal', + 'widget' => array( + 'type' => 'number', + ), + 'display' => array( + 'full' => array( + 'label' => 'above', + 'type' => 'number_decimal', + 'exclude' => 0, + ), + ), +); +field_create_instance($instance); + + === added directory 'modules/field/theme' === added file 'modules/field/theme/field.css' --- modules/field/theme/field.css 1970-01-01 00:00:00 +0000 +++ modules/field/theme/field.css 2009-01-30 23:17:27 +0000 @@ -0,0 +1,30 @@ +/* Node display */ +.field .field-label, +.field .field-label-inline, +.field .field-label-inline-first { + font-weight:bold; +} +.field .field-label-inline, +.field .field-label-inline-first { + display:inline; +} +.field .field-label-inline { + visibility:hidden; +} + +.node-form .field-multiple-table td.field-multiple-drag { + width:30px; + padding-right:0; +} +.node-form .field-multiple-table td.field-multiple-drag a.tabledrag-handle{ + padding-right:.5em; +} + +.node-form .field-add-more .form-submit{ + margin:0; +} + +.node-form .number { + display:inline; + width:auto; +} \ No newline at end of file === added file 'modules/field/theme/field.tpl.php' --- modules/field/theme/field.tpl.php 1970-01-01 00:00:00 +0000 +++ modules/field/theme/field.tpl.php 2009-01-30 23:17:28 +0000 @@ -0,0 +1,49 @@ + + +
+ +
+ +
+ $item) : + if (!$item['empty']) : ?> +
+ +
+
+ + +
+ +
+
+ === modified file 'modules/node/node.module' --- modules/node/node.module 2009-01-28 08:00:08 +0000 +++ modules/node/node.module 2009-01-30 23:17:27 +0000 @@ -152,6 +152,49 @@ function node_cron() { } /** + * Implementation of hook_fieldable_info(). + */ +function node_fieldable_info() { + $return = array( + 'node' => array( + 'name' => t('Node'), + 'id key' => 'nid', + 'revision key' => 'vid', + 'bundle key' => 'type', + // Node.module handles its own caching. + // 'cacheable' => FALSE, + // Bundles must provide human readable name so + // we can create help and error messages about them. + 'bundles' => node_get_types('names'), + ), + ); + return $return; +} + + +/** + * Implementation of hook_field_build_modes(). + */ +function node_field_build_modes($obj_type) { + $modes = array(); + if ($obj_type == 'node') { + $modes = array( + 'teaser' => t('Teaser'), + 'full' => t('Full node'), + NODE_BUILD_RSS => t('RSS'), + NODE_BUILD_PRINT => t('Print'), + ); + if (module_exists('search')) { + $modes += array( + NODE_BUILD_SEARCH_INDEX => t('Search Index'), + NODE_BUILD_SEARCH_RESULT => t('Search Result'), + ); + } + } + return $modes; +} + +/** * Gather a listing of links to nodes. * * @param $result @@ -537,6 +580,9 @@ function node_type_save($info) { if ($is_existing) { db_update('node_type')->fields($fields)->condition('type', $existing_type)->execute(); + if (!empty($type->old_type) && $type->old_type != $type->type) { + field_attach_rename_bundle($type->old_type, $type->type); + } module_invoke_all('node_type', 'update', $type); return SAVED_UPDATED; } @@ -544,6 +590,8 @@ function node_type_save($info) { $fields['orig_type'] = (string) $type->orig_type; db_insert('node_type')->fields($fields)->execute(); + field_attach_create_bundle($type->type); + module_invoke_all('node_type', 'insert', $type); return SAVED_NEW; } @@ -861,6 +909,14 @@ function node_load_multiple($nids = arra } } + // Attach fields. + if ($vid) { + field_attach_load_revision('node', $queried_nodes); + } + else { + field_attach_load('node', $queried_nodes); + } + // Call hook_nodeapi_load(), pass the node types so modules can return early // if not acting on types in the array. foreach (module_implements('nodeapi_load') as $module) { @@ -941,6 +997,9 @@ function node_validate($node, $form = ar } } + // Validate fields + field_attach_validate('node', $node, $form); + // Do node-type-specific validation checks. node_invoke($node, 'validate', $form); node_invoke_nodeapi($node, 'validate', $form); @@ -993,6 +1052,7 @@ function node_submit($node) { * Save a node object into the database. */ function node_save(&$node) { + field_attach_presave('node', $node); // Let modules modify the node before it is saved to the database. node_invoke_nodeapi($node, 'presave'); global $user; @@ -1068,6 +1128,11 @@ function node_save(&$node) { // node_invoke($node, 'insert') or // node_invoke($node, 'update'). node_invoke($node, $op); + + // Save fields. + $function = "field_attach_$op"; + $function('node', $node); + node_invoke_nodeapi($node, $op); // Update the node access table for this node. @@ -1205,6 +1270,9 @@ function node_build_content($node, $teas $node = node_prepare($node, $teaser); } + // Build fields content. + $node->content += field_attach_view('node', $node, $teaser); + // Allow modules to make their own additions to the node. node_invoke_nodeapi($node, 'view', $teaser); === modified file 'modules/node/node.pages.inc' --- modules/node/node.pages.inc 2009-01-27 01:00:13 +0000 +++ modules/node/node.pages.inc 2009-01-30 23:17:28 +0000 @@ -257,7 +257,10 @@ function node_form(&$form_state, $node) ); } $form['#validate'][] = 'node_form_validate'; - $form['#theme'] = array($node->type . '_node_form', 'node_form'); + $form['#theme'] = array($node->type . '_node_form', 'node_form'); + + field_attach_form('node', $node, $form, $form_state); + return $form; } @@ -464,8 +467,11 @@ function node_form_submit_build_node($fo // Unset any button-level handlers, execute all the form-level submit // functions to process the form values into an updated node. unset($form_state['submit_handlers']); - form_execute_handlers('submit', $form, $form_state); - $node = node_submit($form_state['values']); + form_execute_handlers('submit', $form, $form_state); + $node = node_submit($form_state['values']); + + field_attach_submit('node', $node, $form, $form_state); + $form_state['node'] = (array)$node; $form_state['rebuild'] = TRUE; return $node; === modified file 'modules/node/node.tpl.php' --- modules/node/node.tpl.php 2009-01-26 15:00:08 +0000 +++ modules/node/node.tpl.php 2009-01-30 23:17:28 +0000 @@ -20,6 +20,7 @@ * - $terms: the themed list of taxonomy term links output from theme_links(). * - $submitted: themed submission information output from * theme_node_submitted(). + * TODO D7 : document $FIELD_NAME_rendered variables. * * Other variables: * - $node: Full node object. Contains data that may not be safe. === modified file 'modules/simpletest/drupal_web_test_case.php' --- modules/simpletest/drupal_web_test_case.php 2009-01-25 13:00:08 +0000 +++ modules/simpletest/drupal_web_test_case.php 2009-01-31 15:28:45 +0000 @@ -829,7 +829,7 @@ // Add the specified modules to the list of modules in the default profile. $args = func_get_args(); $modules = array_unique(array_merge(drupal_get_profile_modules('default', 'en'), $args)); - drupal_install_modules($modules); + drupal_install_modules($modules, TRUE); // Because the schema is static cached, we need to flush // it between each run. If we don't, then it will contain @@ -1996,4 +1996,58 @@ $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code; return $this->assertTrue($match, $message ? $message : t('HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser')); } + + /** + * TODO write documentation. + * @param $type + * @param $field_name + * @param $settings + * @return unknown_type + */ + protected function drupalCreateField($type, $field_name = NULL, $settings = array()) { + if (!isset($field_name)) { + $field_name = strtolower($this->randomName()); + } + $field_definition = array( + 'field_name' => $field_name, + 'type' => $type, + ); + $field_definition += $settings; + field_create_field($field_definition); + + $field = field_read_field($field_name); + $this->assertTrue($field, t('Created field @field_name of type @type.', array('@field_name' => $field_name, '@type' => $type))); + + return $field; + } + + /** + * TODO write documentation. + * @param $field_name + * @param $widget_type + * @param $display_type + * @param $bundle + * @return unknown_type + */ + protected function drupalCreateFieldInstance($field_name, $widget_type, $formatter_type, $bundle) { + $instance_definition = array( + 'field_name' => $field_name, + 'bundle' => $bundle, + 'widget' => array( + 'type' => $widget_type, + ), + 'display' => array( + 'full' => array( + 'type' => $formatter_type, + ), + ), + ); + field_create_instance($instance_definition); + + $instance = field_read_instance($field_name, $bundle); + $this->assertTrue($instance, t('Created instance of field @field_name on bundle @bundle.', array('@field_name' => $field_name, '@bundle' => $bundle))); + + return $instance; + } } + === added file 'modules/simpletest/tests/field_test.info' --- modules/simpletest/tests/field_test.info 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/field_test.info 2009-01-31 20:12:14 +0000 @@ -0,0 +1,9 @@ +;$Id$ +name = "Field API Test" +description = "Support module for the Field API tests." +core = 7.x +package = testing +files[] = field_test.module +files[] = field_test.install +version = VERSION +hidden = TRUE === added file 'modules/simpletest/tests/field_test.install' --- modules/simpletest/tests/field_test.install 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/field_test.install 2009-01-30 23:17:28 +0000 @@ -0,0 +1,75 @@ + 'The base table for test_entities.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The primary identifier for a test_entity.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'ftvid' => array( + 'description' => 'The current {test_entity_revision}.ftvid version identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'fttype' => array( + 'description' => 'The type of this test_entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'unique keys' => array( + 'ftvid' => array('ftvid'), + ), + 'primary key' => array('ftid'), + ); + $schema['test_entity_revision'] = array( + 'description' => 'Stores information about each saved version of a {test_entity}.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The {test_entity} this version belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'ftvid' => array( + 'description' => 'The primary identifier for this version.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'indexes' => array( + 'nid' => array('ftid'), + ), + 'primary key' => array('ftvid'), + ); + + return $schema; +} + +/** + * Implementation of hook_install(). + */ +function field_test_install() { + drupal_install_schema('field_test'); +} + +/** + * Implementation of hook_uninstall(). + */ +function field_test_uninstall() { + drupal_uninstall_schema('field_test'); +} === added file 'modules/simpletest/tests/field_test.module' --- modules/simpletest/tests/field_test.module 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/field_test.module 2009-01-30 23:17:27 +0000 @@ -0,0 +1,486 @@ + array( + 'title' => t('Access field_test content'), + 'description' => t('View published field_test content.'), + ), + 'administer field_test content' => array( + 'title' => t('Administer field_test content'), + 'description' => t('Manage field_test content'), + ), + ); + return $perms; +} + +/** + * Implementation of hook_menu(). + */ +function field_test_menu() { + $items = array(); + $info = field_test_fieldable_info(); + + foreach (array_keys($info['test_entity']['bundles']) as $bundle) { + $bundle_url_str = str_replace('_', '-', $bundle); + $items['test-entity/add/' . $bundle_url_str] = array( + 'title' => "Add $bundle test_entity", + 'page callback' => 'field_test_entity_add', + 'page arguments' => array(2), + 'access arguments' => array('administer field_test content'), + 'type' => MENU_NORMAL_ITEM, + ); + } + $items['test-entity/%field_test_entity/edit'] = array( + 'title' => 'Edit test entity', + 'page callback' => 'field_test_entity_edit', + 'page arguments' => array(1), + 'access arguments' => array('administer field_test content'), + 'type' => MENU_NORMAL_ITEM, + ); + + return $items; +} + + +/** + * + * 'Field attach' API. + * + */ + + +/** + * Define a test fieldable entity. + */ +function field_test_fieldable_info() { + $bundles = variable_get('field_test_bundles', array('test_bundle' => 'Test Bundle')); + return array( + 'test_entity' => array( + 'name' => t('Test Entity'), + 'id key' => 'ftid', + 'revision key' => 'ftvid', + 'cacheable' => FALSE, + 'bundle key' => 'fttype', + 'bundles' => $bundles, + ), + // This entity type doesn't get form handling for now... + 'test_cacheable_entity' => array( + 'name' => t('Test Entity, cacheable'), + 'id key' => 'ftid', + 'revision key' => 'ftvid', + 'cacheable' => TRUE, + 'bundle key' => 'fttype', + 'bundles' => $bundles, + ), + ); +} + +function field_test_create_bundle($bundle, $text) { + $bundles = variable_get('field_test_bundles', array('field_text_bundle' => 'Test Bundle')); + $bundles += array($bundle => $text); + variable_set('field_test_bundles', $bundles); + + field_attach_create_bundle($bundle); +} + +function field_test_rename_bundle($bundle_old, $bundle_new) { + $bundles = variable_get('field_test_bundles', array('field_text_bundle' => 'Test Bundle')); + $bundles[$bundle_new] = $bundles[$bundle_old]; + unset($bundles[$bundle_old]); + variable_set('field_test_bundles', $bundles); + + field_attach_rename_bundle($bundle_old, $bundle_new); +} + +function field_test_delete_bundle($bundle) { + $bundles = variable_get('field_test_bundles', array('field_text_bundle' => 'Test Bundle')); + unset($bundles[$bundle]); + variable_set('field_test_bundles', $bundles); + + field_attach_delete_bundle($bundle); +} + +/** + * Implementation of hook_field_build_modes(). + */ +function field_test_field_build_modes($obj_type) { + $modes = array(); + if ($obj_type == 'test_entity' || $obj_type == 'test_cacheable_entity') { + $modes = array( + 'full' => t('Full node'), + 'teaser' => t('Teaser'), + ); + } + return $modes; +} + +/** + * Helper function to create a basic 'test entity' structure. + * + * TODO : do we stil need this now that we can actualy load and save test_entities ? + */ +function field_test_create_stub_entity($id = 1, $vid = 1, $bundle = FIELD_TEST_BUNDLE) { + $entity = new stdClass(); + $entity->ftid = $id; + $entity->ftvid = $vid; + $entity->fttype = $bundle; + + return $entity; +} + +function field_test_entity_load($ftid, $ftvid = NULL) { + // Load basic strucure. + $query = db_select('test_entity', 'fte', array()) + ->fields('fte') + ->condition('ftid', $ftid); + if ($ftvid) { + $query->condition('ftvid', $ftvid); + } + $entities = $query->execute()->fetchAllAssoc('ftid'); + + // Attach fields. + if ($ftvid) { + field_attach_load_revision('test_entity', $entities); + } + else { + field_attach_load('test_entity', $entities); + } + + return $entities[$ftid]; +} + +function field_test_entity_save(&$entity) { + field_attach_presave('test_entity', $entity); + + $entity->is_new = FALSE; + if (empty($entity->ftid)) { + // Insert a new test_entity. + $entity->is_new = TRUE; + } + elseif (!empty($entity->revision)) { + $entity->old_ftvid = $entity->ftvid; + } + + $update_entity = TRUE; + if ($entity->is_new) { + drupal_write_record('test_entity', $entity); + drupal_write_record('test_entity_revision', $entity); + $op = 'insert'; + } + else { + drupal_write_record('test_entity', $entity, 'ftid'); + if (!empty($entity->revision)) { + drupal_write_record('test_entity_revision', $entity); + } + else { + drupal_write_record('test_entity_revision', $entity, 'ftvid'); + $update_entity = FALSE; + } + $op = 'update'; + } + if ($update_entity) { + db_update('test_entity') + ->fields(array('ftvid' => $entity->ftvid)) + ->condition('ftid', $entity->ftid) + ->execute(); + } + + // Save fields. + $function = "field_attach_$op"; + $function('test_entity', $entity); +} + +function field_test_entity_add($fttype) { + $fttype = str_replace('-', '_', $fttype); + $entity = (object)array('fttype' => $fttype); + drupal_set_title(t('Create test_entity @bundle', array('@bundle' => $fttype)), PASS_THROUGH); + return drupal_get_form('field_test_entity_form', $entity); +} + +function field_test_entity_edit($entity) { + drupal_set_title(t('test_entity @ftid revision @ftvid', array('@ftid' => $entity->ftid, '@ftvid' => $entity->ftvid)), PASS_THROUGH); + return drupal_get_form('field_test_entity_form', $entity); +} + +/** + * Form to set the value of fields attached to our entity. + */ +function field_test_entity_form(&$form_state, $entity) { + $form = array(); + + foreach (array('ftid', 'ftvid', 'fttype') as $key) { + $form[$key] = array( + '#type' => 'value', + '#value' => isset($entity->$key) ? $entity->$key : NULL, + ); + } + + // Add field widgets. + field_attach_form('test_entity', $entity, $form, $form_state); + + $form['revision'] = array( + '#access' => user_access('administer field_test content'), + '#type' => 'checkbox', + '#title' => t('Create new revision'), + '#default_value' => FALSE, + '#weight' => 100, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => 101, + ); + + return $form; +} + +/** + * Validate handler for field_test_set_field_values(). + */ +function field_test_entity_form_validate($form, &$form_state) { + $entity = (object)$form_state['values']; + field_attach_validate('test_entity', $entity, $form); +} + +/** + * Submit handler for field_test_set_field_values(). + */ +function field_test_entity_form_submit($form, &$form_state) { + $entity = field_test_create_stub_entity($form_state['values']['ftid'], $form_state['values']['ftvid'], $form_state['values']['fttype']); + field_attach_submit('test_entity', $entity, $form, $form_state); + $insert = empty($entity->ftid); + field_test_entity_save($entity); + $message = $insert ? t('test_entity @id has been created.', array('@id' => $entity->ftid)) : t('test_entity @id has been updated.', array('@id' => $entity->ftid)); + drupal_set_message($message); + $form_state['redirect'] = 'test-entity/' . $entity->ftid . '/edit'; +} + +/** + * + * 'Field type' API. + * + */ + +/** + * Implementation of hook_field_info(). + * + * This field provides a textfield which only accepts the value 1. + */ +function field_test_field_info() { + return array( + 'test_field' => array( + 'label' => t('Test Field'), + 'description' => t('Stores the value 1.'), + 'settings' => array('test_field_setting' => 'dummy test string'), + 'instance_settings' => array('test_instance_setting' => 'dummy test string'), + 'default_widget' => 'test_field_widget', + 'default_formatter' => 'field_test_default', + ), + ); +} + +/** + * Implementation of hook_field_columns(). + */ +function field_test_field_columns($field) { + $columns['value'] = array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => FALSE, + ); + return $columns; +} + +/** + * Implementation of hook_instance_settings(). + */ +function field_test_field_instance_settings($field_type) { + return array('test_instance_setting' => 'dummy test string'); +} + +/** + * Implementation of hook_field_validate(). + */ +function field_test_field_validate(&$obj_type, $object, $field, $instance, &$items, $form) { + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if ($item['value'] == -1) { + form_set_error($error_element, t('%name does not accept the value -1.', array('%name' => $instance['label']))); + } + } + } + + return $items; +} + +/** + * Implementation of hook_field_sanitize(). + */ +function field_test_field_sanitize($obj_type, $object, $field, $instance, &$items) { + foreach ($items as $delta => $item) { + $value = check_plain($item['value']); + $items[$delta]['safe'] = $value; + } +} + +/** + * Implementation of hook_field_is_empty(). + */ +function field_test_field_is_empty($item, $field) { + return empty($item['value']); +} + +/** + * Implementation of hook_field_widget_info(). + * + * Here we indicate that the content module will handle + * the default value and multiple values for these widgets. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function field_test_field_widget_info() { + return array( + 'test_field_widget' => array( + 'label' => t('Test field'), + 'field types' => array('test_field'), + 'settings' => array('test_widget_setting' => 'dummy test string'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'test_field_widget_multiple' => array( + 'label' => t('Test field 1'), + 'field types' => array('test_field'), + 'settings' => array('test_widget_setting_multiple' => 'dummy test string'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of hook_field_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * The field structure. + * @param $insatnce + * the insatnce array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function field_test_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = 0) { + $element = array( + 'value' => array( + '#title' => $instance['label'], + '#type' => 'textfield', + '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '', + '#required' => $instance['required'], + ), + ); + return $element; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function field_test_field_formatter_info() { + return array( + 'field_test_default' => array( + 'label' => t('Default'), + 'field types' => array('test_field'), + 'settings' => array( + 'test_formatter_setting' => 'dummy test string', + ), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'field_test_multiple' => array( + 'label' => t('Default'), + 'field types' => array('test_field'), + 'settings' => array( + 'test_formatter_setting_multiple' => 'dummy test string', + ), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + ), + ), + ); +} + +/** + * Implementation of hook_theme(). + */ +function field_test_theme() { + return array( + 'field_formatter_field_test_default' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_field_test_multiple' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Theme function for 'field_test_default' formatter. + */ +function theme_field_formatter_field_test_default($element) { + $value = $element['#item']['value']; + $settings = $element['#settings']; + + return $settings['test_formatter_setting'] . '|' . $value; +} + +/** + * Theme function for 'field_test_multiple' formatter. + */ +function theme_field_formatter_field_test_multiple($element) { + $settings = $element['#settings']; + + $items = array(); + foreach (element_children($element) as $key) { + $items[$key] = $key .':'. $element[$key]['#item']['value']; + } + $output = implode('|', $items); + return $settings['test_formatter_setting_multiple'] . '|' . $output; +} \ No newline at end of file === modified file 'modules/user/user-profile.tpl.php' --- modules/user/user-profile.tpl.php 2008-12-06 09:01:58 +0000 +++ modules/user/user-profile.tpl.php 2009-01-30 23:17:28 +0000 @@ -38,7 +38,8 @@ * Available variables: * - $user_profile: All user profile data. Ready for print. * - $profile: Keyed array of profile categories and their items or other data - * provided by modules. + * provided by modules. + * - TODO D7 : document $FIELD_NAME_rendered variables. * * @see template_preprocess_user_profile() */ === modified file 'modules/user/user.module' --- modules/user/user.module 2009-01-22 13:00:13 +0000 +++ modules/user/user.module 2009-01-31 20:12:14 +0000 @@ -83,6 +83,32 @@ function user_theme() { ); } +/** + * Implementation of hook_fieldable_info(). + */ +function user_fieldable_info() { + $return = array( + 'user' => array( + 'name' => t('User'), + 'id key' => 'uid', + ), + ); + return $return; +} + +/** + * Implementation of hook_field_build_modes(). + */ +function user_field_build_modes($obj_type) { + $modes = array(); + if ($obj_type == 'user') { + $modes = array( + 'full' => t('User account'), + ); + } + return $modes; +} + function user_external_load($authname) { $result = db_query("SELECT uid FROM {authmap} WHERE authname = '%s'", $authname); @@ -187,6 +213,10 @@ function user_load($array = array()) { $user->roles[$role->rid] = $role->name; } + // Attach fields. + // TODO D7 : not sure the 3rd param ($types) is needed. + field_attach_load('user', array($user->uid => $user)); + if (!empty($user->picture) && ($file = file_load($user->picture))) { $user->picture = $file; } @@ -241,6 +271,18 @@ function user_save($account, $edit = arr unset($edit['pass']); } + // Get the fields form so we can recognize the fields in the $edit + // form that should not go into the serialized data array. + $field_form = array(); + $field_form_state = array(); + $edit = (object) $edit; + field_attach_form('user', $edit, $field_form, $field_form_state); + + // Presave fields. + field_attach_presave('user', $edit); + + $edit = (array) $edit; + if (is_object($account) && $account->uid) { user_module_invoke('update', $edit, $account, $category); $data = unserialize(db_result(db_query('SELECT data FROM {users} WHERE uid = %d', $account->uid))); @@ -250,9 +292,10 @@ function user_save($account, $edit = arr $edit['access'] = REQUEST_TIME; } foreach ($edit as $key => $value) { - // Fields that don't pertain to the users or user_roles - // automatically serialized into the users.data column. - if ($key != 'roles' && empty($user_fields[$key])) { + // Form fields that don't pertain to the users, user_roles, or + // Field API are automatically serialized into the users.data + // column. + if ($key != 'roles' && empty($user_fields[$key]) && empty($field_form[$key])) { if ($value === NULL) { unset($data[$key]); } @@ -284,8 +327,14 @@ function user_save($account, $edit = arr // Save changes to the users table. $success = drupal_write_record('users', $edit, 'uid'); if (!$success) { - // The query failed - better to abort the save than risk further data loss. - return FALSE; + // The query failed - better to abort the save than risk further + // data loss. + + // TODO: Fields change: I think this is a bug. If no columns in + // the users table are changed, drupal_write_record returns + // FALSE because rowCount() (rows changed) is 0. However, + // non-users data may have been changed, e.g. fields. + // return FALSE; } // If the picture changed or was unset, remove the old one. This step needs @@ -320,6 +369,10 @@ function user_save($account, $edit = arr } } + // Save Field data. + $obj = (object) $edit; + field_attach_update('user', $obj); + // Refresh user object. $user = user_load(array('uid' => $account->uid)); @@ -353,13 +406,19 @@ function user_save($account, $edit = arr // Build the initial user object. $user = user_load(array('uid' => $edit['uid'])); + $obj = (object) $edit; + field_attach_insert('user', $obj); + user_module_invoke('insert', $edit, $user, $category); // Note, we wait with saving the data column to prevent module-handled // fields from being saved there. $data = array(); foreach ($edit as $key => $value) { - if (($key != 'roles') && (empty($user_fields[$key])) && ($value !== NULL)) { + // Form fields that don't pertain to the users, user_roles, or + // Field API are automatically serialized into the users.data + // column. + if (($key != 'roles') && (empty($user_fields[$key]) && empty($field_form[$key])) && ($value !== NULL)) { $data[$key] = $value; } } @@ -1594,6 +1653,8 @@ function user_edit_form(&$form_state, $u _user_password_dynamic_validation(); $admin = user_access('administer users'); + $form = array(); + // Account information: $form['account'] = array('#type' => 'fieldset', '#title' => t('Account information'), @@ -1829,7 +1890,15 @@ function _user_cancel($edit, $account, $ */ function user_build_content(&$account) { $edit = NULL; + $account->content = array(); + + // Build fields content. + // TODO D7 : figure out where exactly this needs to go + // TODO D7 : $page / $teaser ?? + $account->content += field_attach_view('user', $account); + user_module_invoke('view', $edit, $account); + // Allow modules to modify the fully-built profile. drupal_alter('profile', $account); === modified file 'modules/user/user.pages.inc' --- modules/user/user.pages.inc 2009-01-22 05:00:13 +0000 +++ modules/user/user.pages.inc 2009-01-30 23:17:27 +0000 @@ -174,6 +174,9 @@ function template_preprocess_user_profil } // Collect all profiles to make it easier to print all items at once. $variables['user_profile'] = implode($variables['profile']); + + // Add $FIELD_NAME_rendered variables for fields. + $variables += field_attach_preprocess('user', $variables['account']); } /** @@ -236,6 +239,10 @@ function user_profile_form($form_state, $edit = (empty($form_state['values'])) ? (array)$account : $form_state['values']; $form = _user_forms($edit, $account, $category); + + // Attach field widgets. + field_attach_form('user', (object) $edit, $form, $form_state); + $form['_category'] = array('#type' => 'value', '#value' => $category); $form['_account'] = array('#type' => 'value', '#value' => $account); $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 30); @@ -256,6 +263,10 @@ function user_profile_form($form_state, * Validation function for the user account and profile editing form. */ function user_profile_form_validate($form, &$form_state) { + // Validate field widgets. + $tmp_obj = (object) $form_state['values']; + field_attach_validate('user', $tmp_obj, $form, $form_state); + user_module_invoke('validate', $form_state['values'], $form_state['values']['_account'], $form_state['values']['_category']); // Validate input to ensure that non-privileged users can't alter protected data. if ((!user_access('administer users') && array_intersect(array_keys($form_state['values']), array('uid', 'init', 'session'))) || (!user_access('administer permissions') && isset($form_state['values']['roles']))) { @@ -272,6 +283,8 @@ function user_profile_form_submit($form, $account = $form_state['values']['_account']; $category = $form_state['values']['_category']; unset($form_state['values']['_account'], $form_state['values']['op'], $form_state['values']['submit'], $form_state['values']['cancel'], $form_state['values']['form_token'], $form_state['values']['form_id'], $form_state['values']['_category'], $form_state['values']['form_build_id']); + + field_attach_submit('user', $account, $form, $form_state); user_module_invoke('submit', $form_state['values'], $account, $category); user_save($account, $form_state['values'], $category);