From 7e0ac72020c8872a04bac572fac4fdbbe23e3d7a Mon Sep 17 00:00:00 2001 From: Michael Cole dev Date: Fri, 3 Feb 2012 14:59:25 -0600 Subject: [PATCH] Reorganize and update code --- entity_example/entity_example.entity.inc | 290 ++++++++++++++++++++++++++++++ entity_example/entity_example.fapi.inc | 208 +++++++++++++++++++++ entity_example/entity_example.info | 10 + entity_example/entity_example.install | 67 +++++++ entity_example/entity_example.module | 148 +++++++++++++++ entity_example/entity_example.test | 150 +++++++++++++++ 6 files changed, 873 insertions(+), 0 deletions(-) create mode 100644 entity_example/entity_example.entity.inc create mode 100644 entity_example/entity_example.fapi.inc create mode 100644 entity_example/entity_example.info create mode 100644 entity_example/entity_example.install create mode 100644 entity_example/entity_example.module create mode 100644 entity_example/entity_example.test diff --git a/entity_example/entity_example.entity.inc b/entity_example/entity_example.entity.inc new file mode 100644 index 0000000..1e2d727 --- /dev/null +++ b/entity_example/entity_example.entity.inc @@ -0,0 +1,290 @@ + t('Example Basic Entity'), + + // The controller for our Entity, extending the Drupal core controller. + 'controller class' => 'EntityExampleBasicController', + + // The table for this entity defined in hook_schema() + 'base table' => 'entity_example_basic', + + // Returns the uri elements of an entity + 'uri callback' => 'entity_example_basic_uri', + + // IF fieldable == FALSE, we can't attach fields. + 'fieldable' => TRUE, + + // entity_keys tells the controller what database fields are used for key + // functions. It is not required if we don't have bundles or revisions. + // Here we do not support a revision, so that entity key is omitted. + 'entity keys' => array( + 'id' => 'basic_id' , // The 'id' (basic_id here) is the unique id. + 'bundle' => 'bundle_type' // Bundle will be determined by the 'bundle_type' field + ), + 'bundle keys' => array( + 'bundle' => 'bundle_type', + ), + + // FALSE disables caching. Caching functionality is handled by Drupal core. + 'static cache' => TRUE, + + // Bundles are alternative groups of fields or configuration + // associated with a base entity type. + 'bundles' => array( + 'first_example_bundle' => array( + 'label' => 'First example bundle', + // 'admin' key is used by the Field UI to provide field and + // display UI pages. + 'admin' => array( + 'path' => 'admin/structure/entity_example_basic/manage', + 'access arguments' => array('administer entity_example_basic entities'), + ), + ), + ), + // View modes allow entities to be displayed differently based on context. + // As a demonstration we'll support "Tweaky", but we could have and support + // multiple display modes. + 'view modes' => array( + 'tweaky' => array( + 'label' => t('Tweaky'), + 'custom settings' => FALSE, + ), + ) + ); + + return $info; +} + +/** + * EntityExampleBasicControllerInterface definition. + * + * We create an interface here because anyone could come along and + * use hook_entity_info_alter() to change our controller class. + * We want to let them know what methods our class needs in order + * to function with the rest of the module, so here's a handy list. + * + * @see hook_entity_info_alter() + */ + +interface EntityExampleBasicControllerInterface + extends DrupalEntityControllerInterface { + public function create(); + public function save($entity); + public function delete($entity); +} + +/** + * EntityExampleBasicController extends DrupalDefaultEntityController. + * + * Our subclass of DrupalDefaultEntityController lets us add a few + * important create, update, and delete methods. + */ +class EntityExampleBasicController + extends DrupalDefaultEntityController + implements EntityExampleBasicControllerInterface { + + /** + * Create and return a new entity_example_basic entity. + */ + public function create() { + $entity = new stdClass(); + $entity->type = 'entity_example_basic'; + $entity->basic_id = 0; + $entity->bundle_type = 'first_example_bundle'; + $entity->item_description = ''; + return $entity; + } + + /** + * Saves the custom fields using drupal_write_record() + */ + public function save($entity) { + // If our entity has no basic_id, then we need to give it a + // time of creation. + if (empty($entity->basic_id)) { + $entity->created = time(); + } + // Invoke hook_entity_presave(). + module_invoke_all('entity_presave', 'entity_example_basic', $entity); + // The 'primary_keys' argument determines whether this will be an insert + // or an update. So if the entity already has an ID, we'll specify + // basic_id as the key. + $primary_keys = $entity->basic_id ? 'basic_id' : array(); + // Write out the entity record. + drupal_write_record('entity_example_basic', $entity, $primary_keys); + // We're going to invoke either hook_entity_update() or + // hook_entity_insert(), depending on whether or not this is a + // new entity. We'll just store the name of hook_entity_insert() + // and change it if we need to. + $invocation = 'entity_insert'; + // Now we need to either insert or update the fields which are + // attached to this entity. We use the same primary_keys logic + // to determine whether to update or insert, and which hook we + // need to invoke. + if (empty($primary_keys)) { + field_attach_insert('entity_example_basic', $entity); + } + else { + field_attach_update('entity_example_basic', $entity); + $invocation = 'entity_update'; + } + // Invoke either hook_entity_update() or hook_entity_insert(). + module_invoke_all($invocation, 'entity_example_basic', $entity); + return $entity; + } + + /** + * Delete a single entity. + * + * Really a convenience function for delete_multiple(). + */ + public function delete($entity) { + $this->delete_multiple(array($entity)); + } + + /** + * Delete one or more entity_example_basic entities. + * + * Deletion is unfortunately not supported in the base + * DrupalDefaultEntityController class. + * + * @param $basic_ids + * An array of entity IDs or a single numeric ID. + */ + public function delete_multiple($entities) { + $basic_ids = array(); + if (!empty($entities)) { + $transaction = db_transaction(); + try { + foreach ($entities as $entity) { + module_invoke_all('entity_example_basic_delete', $entity); + // Invoke hook_entity_delete(). + module_invoke_all('entity_delete', $entity, 'entity_example_basic'); + field_attach_delete('entity_example_basic', $entity); + $basic_ids[] = $entity->basic_id; + } + db_delete('entity_example_basic') + ->condition('basic_id', $basic_ids, 'IN') + ->execute(); + + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception('entity_example', $e); + throw $e; + } + } + } +} + +/** + * We save the entity by calling the controller. + */ +function entity_example_basic_save(&$entity) { + return entity_get_controller('entity_example_basic')->save($entity); +} + +/** + * Use the controller to delete the entity. + */ +function entity_example_basic_delete($entity) { + entity_get_controller('entity_example_basic')->delete($entity); +} + +/** + * Fetch a basic object. + * + * This function ends up being a shim between the menu system and + * entity_example_basic_load_multiple(). + * + * This function gets its name from the menu system's wildcard + * naming conventions. For example, /path/%wildcard would end + * up calling wildcard_load(%wildcard value). In our case defining + * the path: examples/entity_example/basic/%entity_example_basic in + * hook_menu() tells Drupal to call entity_example_basic_load(). + * + * @param $basic_id + * Integer specifying the basic entity id. + * @param $reset + * A boolean indicating that the internal cache should be reset. + * @return + * A fully-loaded $basic object or FALSE if it cannot be loaded. + * + * @see entity_example_basic_load_multiple() + * @see entity_example_menu() + */ +function entity_example_basic_load($basic_id = NULL, $reset = FALSE) { + $basic_ids = (isset($basic_id) ? array($basic_id) : array()); + $basic = entity_example_basic_load_multiple($basic_ids, $reset); + return $basic ? reset($basic) : FALSE; +} + +/** + * Loads multiple basic entities. + * + * We only need to pass this request along to entity_load(), which + * will in turn call the load() method of our entity controller class. + */ +function entity_example_basic_load_multiple($basic_ids = FALSE, $conditions = array(), $reset = FALSE) { + return entity_load('entity_example_basic', $basic_ids, $conditions, $reset); +} + +/** + * Implements the uri callback. + */ +function entity_example_basic_uri($basic) { + return array( + 'path' => 'examples/entity_example/basic/' . $basic->basic_id, + ); +} + +/** + * Implements hook_field_extra_fields() + * + * This exposes the "extra fields" (usually properties that can be configured + * as if they were fields) of the entity as pseudo-fields + * so that they get handled by the Entity and Field core functionality. + * Node titles get treated in a similar manner. + */ +function entity_example_field_extra_fields() { + $form_elements['item_description'] = array( + 'label' => t('Item Description'), + 'description' => t('Item Description (an extra form field)'), + 'weight' => -5, + ); + $display_elements['created'] = array( + 'label' => t('Creation date'), + 'description' => t('Creation date (an extra display field)'), + 'weight' => 0, + ); + $display_elements['item_description'] = array( + 'label' => t('Item Description'), + 'description' => t('Just like title, but trying to point out that it is a separate property'), + 'weight' => 0, + ); + + // Since we have only one bundle type, we'll just provide the extra_fields + // for it here. + $extra_fields['entity_example_basic']['first_example_bundle']['form'] = $form_elements; + $extra_fields['entity_example_basic']['first_example_bundle']['display'] = $display_elements; + + return $extra_fields; +} + diff --git a/entity_example/entity_example.fapi.inc b/entity_example/entity_example.fapi.inc new file mode 100644 index 0000000..bbd9137 --- /dev/null +++ b/entity_example/entity_example.fapi.inc @@ -0,0 +1,208 @@ +create(); + return drupal_get_form('entity_example_basic_form', $entity); +} + +// 'Edit' entity + +/** + * Form function to create an entity_example_basic entity. + * + * The pattern is: + * - Set up the form for the data that is specific to your + * entity: the columns of your base table. + * - Call on the Field API to pull in the form elements + * for fields attached to the entity. + */ +function entity_example_basic_form($form, &$form_state, $entity) { + $form['item_description'] = array( + '#type' => 'textfield', + '#title' => t('Item Description'), + '#required' => TRUE, + '#default_value' => $entity->item_description, + ); + + $form['basic_entity'] = array( + '#type' => 'value', + '#value' => $entity, + ); + + field_attach_form('entity_example_basic', $entity, $form, $form_state); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => 100, + ); + $form['delete'] = array( + '#type' => 'submit', + '#value' => t('Delete'), + '#submit' => array('entity_example_basic_edit_delete'), + '#weight' => 200, + ); + + return $form; +} + + +/** + * Validation handler for entity_example_basic_add_form form. + * We pass things straight through to the Field API to handle validation + * of the attached fields. + */ +function entity_example_basic_form_validate($form, &$form_state) { + field_attach_form_validate('entity_example_basic', $form_state['values']['basic_entity'], $form, $form_state); +} + + +/** + * Form submit handler: submits basic_add_form information + */ +function entity_example_basic_form_submit($form, &$form_state) { + $entity = $form_state['values']['basic_entity']; + $entity->item_description = $form_state['values']['item_description']; + field_attach_submit('entity_example_basic', $entity, $form, $form_state); + $entity = entity_example_basic_save($entity); + $form_state['redirect'] = 'examples/entity_example/basic/' . $entity->basic_id; +} + +// 'View' entity + +/** + * Callback for a page title when this entity is displayed. + */ +function entity_example_basic_title($entity) { + return t('Entity Example Basic (item_description=@item_description)', array('@item_description' => $entity->item_description)); +} + +/** + * Function to display the entity. + */ +function entity_example_basic_view($entity, $view_mode = 'tweaky') { + + $entity->content = array( + '#view_mode' => $view_mode, + ); + + // Build fields content - this where the FieldAPI really comes in to play. + // The task has very little code here because it all gets taken care of by + // field module. + field_attach_prepare_view('entity_example_basic', array($entity->basic_id => $entity), $view_mode); + entity_prepare_view('entity_example_basic', array($entity->basic_id => $entity)); + $entity->content += field_attach_view('entity_example_basic', $entity, $view_mode); + + $entity->content['created'] = array( + '#type' => 'item', + '#title' => t('Created date'), + '#markup' => format_date($entity->created), + ); + $entity->content['item_description'] = array( + '#type' => 'item', + '#title' => t('Item Description'), + '#markup' => $entity->item_description, + ); + + $type = 'entity_example_basic'; + drupal_alter(array('entity_example_basic_view', 'entity_view'), $entity->content, $type); + + return $entity->content; +} + + +// 'List' entities + +/** + * Provides a list of existing entities and the ability to add more. Tabs + * provide field and display management. + */ +function entity_example_basic_admin_page() { + $content = array(); + $content[] = array( + '#type' => 'item', + '#markup' => t('Administration page for Entity Example Basic Entities.') + ); + + $content[] = array( + '#type' => 'item', + '#markup' => l(t('Add an entity_example_basic entity'), 'examples/entity_example/basic/add'), + ); + + $content['table'] = entity_example_basic_list_entities(); + + return $content; +} + +/** + * Returns a render array with all entity_example_basic entities. + */ +function entity_example_basic_list_entities() { + $content = array(); + + if (!user_access('view any entity_example_basic entity')) { + $content[] = array( + '#type' => 'item', + '#markup' => t('This user does not have permission to view entity_example_basic entities.'), + ); + return $content; + } + // In this basic example we know that there won't be many entities, + // so just load them all. @see pager_example.module to implement a pager. + // Most implementations would probably do this with a view using views module, + // but we avoid using non-core features in the Examples project. + $entities = entity_example_basic_load_multiple(); + if (!empty($entities)) { + foreach ( $entities as $entity ) { + $rows[] = array( + 'data' => array( + 'id' => $entity->basic_id, + 'item_description' => l($entity->item_description, 'examples/entity_example/basic/' . $entity->basic_id), + 'bundle' => $entity->bundle_type, + ), + ); + } + $content['entity_table'] = array( + '#theme' => 'table', + '#rows' => $rows, + '#header' => array(t('ID'), t('Item Description'), t('Bundle')), + ); + } + else { + $content[] = array( + '#type' => 'item', + '#markup' => t('No entity_example_basic entities currently exist.'), + ); + } + return $content; +} + +// 'Delete' entity + +/** + * Form deletion handler. + * + * @todo: 'Are you sure?' message. + */ +function entity_example_basic_edit_delete( $form , &$form_state ) { + $entity = $form_state['values']['basic_entity']; + entity_example_basic_delete($entity); + drupal_set_message(t('The entity %item_description (ID %id) has been deleted', + array('%item_description' => $entity->item_description, '%id' => $entity->basic_id)) + ); + $form_state['redirect'] = 'examples/entity_example'; +} + diff --git a/entity_example/entity_example.info b/entity_example/entity_example.info new file mode 100644 index 0000000..262c776 --- /dev/null +++ b/entity_example/entity_example.info @@ -0,0 +1,10 @@ +name = Entity Example +description = A simple entity example showing the main steps required to set up your own entity. +core = 7.x +package = Example modules +dependencies[] = field +files[] = entity_example.install +files[] = entity_example.module +files[] = entity_example.entity.inc +files[] = entity_example.fapi.inc +files[] = entity_example.test diff --git a/entity_example/entity_example.install b/entity_example/entity_example.install new file mode 100644 index 0000000..1c1db83 --- /dev/null +++ b/entity_example/entity_example.install @@ -0,0 +1,67 @@ + 'The base table for our basic entity.', + 'fields' => array( + 'basic_id' => array( + 'description' => 'Primary key of the basic entity.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + // If we allow multiple bundles, then the schema must handle that; + // We'll put it in the 'bundle_type' field to avoid confusion with the + // entity type. + 'bundle_type' => array( + 'description' => 'The bundle type', + 'type' => 'text', + 'size' => 'medium', + 'not null' => TRUE + ), + // Additional properties are just things that are common to all + // entities and don't require field storage. + 'item_description' => array( + 'description' => 'A description of the item', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'created' => array( + 'description' => 'The Unix timestamp of the entity creation time.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('basic_id'), + ); + + return $schema; +} + + +/* + * Implements hook_uninstall(). + * + * At uninstall time we'll notify field.module that the entity was deleted + * so that attached fields can be cleaned up. + */ +function entity_example_uninstall(){ + field_attach_delete_bundle('entity_example_basic' , 'first_example_bundle' ); +} diff --git a/entity_example/entity_example.module b/entity_example/entity_example.module new file mode 100644 index 0000000..eb503bc --- /dev/null +++ b/entity_example/entity_example.module @@ -0,0 +1,148 @@ + 'Entity Example', + 'page callback' => 'entity_example_info_page', + 'access callback' => TRUE, + ); + + // Entity Add/View/Edit/List menu paths: + + // 'Add' example entity. + $items['examples/entity_example/basic/add'] = array( + 'title' => 'Add an Entity Example Basic Entity', + 'page callback' => 'entity_example_basic_add', + 'access arguments' => array('create entity_example_basic entities'), + ); + + // 'View' tab for an individual entity page. + $items['examples/entity_example/basic/%entity_example_basic/view'] = array( + 'title' => 'View', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + + // 'Edit' tab for an individual entity page. + $items['examples/entity_example/basic/%entity_example_basic/edit'] = array( + 'title' => 'Edit', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('entity_example_basic_form', 3), + 'access arguments' => array('edit any entity_example_basic entity'), + 'type' => MENU_LOCAL_TASK, + ); + + // 'List' page for entities + // The page to view our entities - needs to follow what + // is defined in basic_uri and will use load_basic to retrieve + // the necessary entity info. + $items['examples/entity_example/basic/%entity_example_basic'] = array( + 'title callback' => 'entity_example_basic_title', + 'title arguments' => array(3), + 'page callback' => 'entity_example_basic_view', + 'page arguments' => array(3), + 'access arguments' => array('view any entity_example_basic entity'), + ); + + // Entity Admin menu paths: + + // This provides a place for Field API to hang its own + // interface and has to be the same as what was defined + // in basic_entity_info() above. + $items['admin/structure/entity_example_basic/manage'] = array( + 'title' => 'Administer entity_example_basic entity type', + 'page callback' => 'entity_example_basic_admin_page', + 'access arguments' => array('administer entity_example_basic entities'), + ); + + return $items; +} + +// Basic Module Code + +/** + * Implements hook_permission() + */ +function entity_example_permission() { + $permissions = array( + 'administer entity_example_basic entities' => array( + 'title' => t('Administer entity_example_basic entities'), + ), + 'view any entity_example_basic entity' => array( + 'title' => t('View any Entity Example Basic entity'), + ), + 'edit any entity_example_basic entity' => array( + 'title' => t('Edit any Entity Example Basic entity'), + ), + 'create entity_example_basic entities' => array( + 'title' => t('Create Entity Example Basic Entities'), + ), + ); + return $permissions; +} + +/** + * Basic information for the page. + * + * @todo: Give links to admin pages, etc. + */ +function entity_example_info_page() { + $content[] = array( + '#type' => 'item', + '#markup' => t('The entity example provides a simple example entity. You can administer these and add fields and change the view !link.', + array('!link' => l(t('here'), 'admin/structure/entity_example_basic/manage')) + ), + ); + $content['table'] = entity_example_basic_list_entities(); + + return $content; +} + diff --git a/entity_example/entity_example.test b/entity_example/entity_example.test new file mode 100644 index 0000000..3be2ef3 --- /dev/null +++ b/entity_example/entity_example.test @@ -0,0 +1,150 @@ + 'Entity example', + 'description' => 'Basic entity example tests', + 'group' => 'Examples', + ); + } + + function setUp() { + // Enable the module. + parent::setUp('entity_example'); + + // Create and login user with access. + $permissions = array( + 'access content', + 'view any entity_example_basic entity', + 'edit any entity_example_basic entity', + 'create entity_example_basic entities', + 'administer entity_example_basic entities', + 'administer site configuration', + ); + $account = $this->drupalCreateUser($permissions); + $this->drupalLogin($account); + + // Attach a field + $field = array( + 'field_name' => 'entity_example_test_text' , + 'type' => 'text' + ); + field_create_field($field); + $instance = array( + 'label' => 'Subject', + 'field_name' => 'entity_example_test_text', + 'entity_type' => 'entity_example_basic', + 'bundle' => 'first_example_bundle' + ); + field_create_instance($instance); + } + + /** + * Test Entity Example features. + * + * CRUD + * Table display + * User access + * Field management + * Display management + */ + function testEntityExampleBasic() { + // Create 10 entities. + for ($i = 1; $i <= 10; $i++) { + $edit[$i]['item_description'] = $this->randomName(); + $edit[$i]['entity_example_test_text[und][0][value]'] = $this->randomName(32); + + $this->drupalPost('examples/entity_example/basic/add' , $edit[$i], 'Save'); + $this->assertText('item_description=' . $edit[$i]['item_description']); + + $this->drupalGet('examples/entity_example/basic/' . $i); + $this->assertText('item_description=' . $edit[$i]['item_description']); + $this->assertText($edit[$i]['entity_example_test_text[und][0][value]']); + } + + // delete entity 5 + $this->drupalPost('examples/entity_example/basic/5/edit' , $edit[5], 'Delete'); + $this->drupalGet('examples/entity_example/basic/5'); + $this->assertResponse(404, t('Deleted entity 5 no longer exists')); + unset($edit[5]); + + // Update entity 2 and verify the update. + $edit[2] = array( + 'item_description' => 'updated entity 2 ', + 'entity_example_test_text[und][0][value]' => 'updated entity 2 test text', + ); + $this->drupalPost('examples/entity_example/basic/2/edit' , $edit[2], 'Save'); + $this->assertText('item_description=' . $edit[2]['item_description']); + $this->assertText('updated entity 2 test text'); + + // View the entity list page and verify that the items which still exist + // are there, and that the deleted #5 no longer is there. + $this->drupalGet('admin/structure/entity_example_basic/manage'); + foreach ($edit as $id => $item) { + $this->assertRaw('examples/entity_example/basic/' . $id . '">' . $item['item_description'] . ''); + } + $this->assertNoRaw('examples/entity_example/basic/5">'); + + // Add a field through the field UI and verify that it behaves correctly. + $field_edit = array( + 'fields[_add_new_field][label]' => 'New junk field', + 'fields[_add_new_field][field_name]' => 'new_junk_field', + 'fields[_add_new_field][type]' => 'text', + 'fields[_add_new_field][widget_type]' => 'text_textfield', + ); + $this->drupalPost('admin/structure/entity_example_basic/manage/fields', $field_edit, t('Save')); + $this->drupalPost(NULL, array(), t('Save field settings')); + $this->drupalPost(NULL, array(), t('Save settings')); + $this->assertResponse(200); + + // Now verify that we can edit and view this entity with fields. + $edit[10]['field_new_junk_field[und][0][value]'] = $this->randomName(); + $this->drupalPost('examples/entity_example/basic/10/edit' , $edit[10], 'Save'); + $this->assertResponse(200); + $this->assertText('item_description=' . $edit[10]['item_description']); + $this->assertText($edit[10]['field_new_junk_field[und][0][value]'], t('Custom field updated successfully')); + + // Create and login user without view access. + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet('admin/structure/entity_example_basic/manage'); + $this->assertResponse(403); + $this->drupalGet('examples/entity_example/basic/2'); + $this->assertResponse(403, t('User does not have permission to view entity')); + + // Create and login user with view access but no edit access. + $account = $this->drupalCreateUser(array('access content', 'view any entity_example_basic entity')); + $this->drupalLogin($account); + $this->drupalGet('admin/structure/entity_example_basic/manage'); + $this->assertResponse(403, t('Denied access to admin manage page')); + $this->drupalGet('examples/entity_example/basic/2'); + $this->assertResponse(200, t('User has permission to view entity')); + $this->drupalGet('examples/entity_example/basic/2/edit'); + $this->assertResponse(403, t('User is denied edit privileges')); + + // Create and login user with view and edit but no manage privs. + $account = $this->drupalCreateUser(array('access content', 'view any entity_example_basic entity', 'edit any entity_example_basic entity')); + $this->drupalLogin($account); + $this->drupalGet('admin/structure/entity_example_basic/manage'); + $this->assertResponse(403, t('Denied access to admin manage page')); + $this->drupalGet('examples/entity_example/basic/2'); + $this->assertResponse(200, t('User has permission to view entity')); + $this->drupalGet('examples/entity_example/basic/2/edit'); + $this->assertResponse(200, t('User has edit privileges')); + + + } +} + -- 1.7.4.1