=== 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
+ *
' .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 @@ + + +