Importing and Exporting Products in Drupal Commerce

In prevous post Drupal Commerce Install and Setup, we went over on incorporating commerce functionality with Drupal and creating/displaying Products. In this post, we go over how to import/export Drupal commerce “products” as well as “product displays”. Important to note, there are “products” and then there are “product displays”. The “product display” is displaying one to many products referenced by the display. It is plain Drupal node with an extra field of type – product reference. The “product” is specific type provided by Commerce module.

Initially, I was thinking to use features for import and export products in drupal commerce store but it doesn’t have the capability for products to be featured, so the solution for us was using Views data export to export products and Commerce_feeds for importing products

Exporting Commerce Products

So, you have several products that you created with Drupal Commerce product installed and its UI. To export those products, we first create a view containing the products and then export this view via Views data export into CSV file

a) Install views_data_export and dependent modules
drush dl views_data_export
drush dl image_url_formatter
drush en views_data_export image_url_formatter
b) Create a view with products you like to export

Create a view with products you like to export. The view has to list each product field you like to export, so add each product field. Here are other specifics of the view:

  • ‘size’ is set to unlimited
  • give a display name in the Administration settings(in our example ‘theme_export_display’)
  • select image fields with field formatter ‘image_url’ (see module image_url_formatter) with setting ‘nothing’ for linking
    • Image url and ‘URI Path’ for Uri path.
  • ensure price is of type “raw amount”(i.e 8000 for $80.00)
  • ensure product status output format is ‘0/1’
  • Do not include Product ID.
  • keep field labels, so there is header in the export file
  • Format type for taxonomy fields are ‘link’
b) Configure View for export
  1. Add a new “Data export” display to your view.
  2. Change its “Style” to the desired export type. e.g. “CSV file”.
  3. Configure the options (such as name, quote, etc.). You can go back and do
    this at any time by clicking the gear icon next to the style plugin you just
    selected.
  4. Give it a path in the Feed settings such as “path/to/view/csv”.
  5. Attach this view to the view containing the products you like to export by
    updating the “Attach to:” option in feed settings.
  6. Ensure “Items to Display” is set to “Display All”
c) Generate the file via drush command
drush views-data-export --format=csv VIEW-NAME VIEW-DISPLAY-NAME exports/product-exports/theme_export_Jan11_2014.csv --quote-values --strict=0 --header-row

This will take the view and create csv file from it.

Import Commerce Products

Once you have the csv file with commerce products an export generated as described above, we going to use Commerce_feeds for import this products in another Drupal instance

a) install commerce_feeds module and its dependent modules
drush dl feeds
drush dl feeds_ui
drush dl commerce_feeds
drush en feeds feeds_ui commerce_feeds

This install all the necessary artifacts for using commerce_feeds to import Drupal commerce products

b)Create New Feed Importer

Create a new feed importer named “Product Importer” at Administration -> Structure -> Feeds Importer -> Add Importer. Here are specifics of this importer:

  • Change the parser to “CSV Parser”
  • Change processor to “Commerce Product Processor”
  • In “Commerce Product Processor” settings use product type “product” (or whatever your product is) and change the “Author” to your username.
  • ensure Fetcher is of type ‘file upload’
  • In “mapping”, map:
    • SKU -> Product SKU
    • Title -> Product Title
    • Price -> Price: Amount
    • Image -> Image
    • Set ID as unique target.
    • …//so on with other product fields in the .csv file

c) Run import

Go to ‘/import’ url, use the Product Importer and import your products
Note: before importing, make sure you have copied the artifacts into the same location you importing from if there is any custom fields with artifacts

Exporting Product Displays

Everything would be the same if there wouldn’t be product reference field that links the multiple products to the particular display it is for. To import/export ‘product’ field, we will utilize modules ‘feeds_temper’ and ‘feeds_tamper_ui’ modules.

drush dl feeds_tamper
drush en feeds_tamper feeds_tamper_ui

Their responsibility is to take list of SKUs part of ‘products’ field and create multiple product references for each display at the time of import

a)Create a view

Create view with displays to export(i.e. view ‘theme_display_export’ with display name – ‘display_export’). Here are some important details about the view:

  • ensure result size of node is set to unlimited
  • ensure the output formatter for field ‘product’ is set ot ‘SKU no link’
  • ensure label is attached, so it generates csv with a header row

b) Configure View for export
  1. Add a new “Data export” display to your view.
  2. Change its “Style” to the desired export type. e.g. “CSV file”.
  3. Configure the options (such as name, quote, etc.)
  4. Give it a path in the Feed settings such as “path/to/view/csv”.
  5. Attach this view to the view containing the product displays you like to export by
    updating the “Attach to:” option in feed settings.
  6. Ensure “Items to Display” is set to “Display All”
c)Run Export

Export Product Displays into .csv file by calling drush command as following:

drush views-data-export --format=csv theme_display_export display_export exports/display-exports/display_export_Jan11_2014.csv --quote-values --strict=0 --header-row

This will generate .csv file with product displays

Import Product Displays

Once you have the .csv of product displays, we import them in another Drupal instance. Here are the steps:

a)Create Product Display Importer

First, create product display Importer by going to ‘Admin’->’Structure’->’Feeds Importer’ -> ‘Add New Importer’. Some important things to watch for:

  • ensure this is Nodes Processor under the Process
  • ensure Fetcher is set to ‘file upload’
  • create mappings
b)Create Rule for ‘product’ field

Create Rule for ‘product’ field by clicking on the ‘Tamper’ tab and then ‘add plugin’ under the field ‘product’ to ‘Product:SKU’ mapping or however you named the relationship mapping between display to products.
Note: ensure the rule is of type ‘list/explode’

c)Run importer

At last, run importer by going to ‘/importer’ url and selecting the product display importer following the UI

Troubleshooting

1. Unknown option: –header-row. See `drush help views-data-export` for available options.To suppress this error, add the option –strict=0

Make sure to add –strict=0 to the command or in the ‘alias’ configuration file

2. Exported file’s header comes up empty(“”,””,””…)

Make sure to include label for each field, otherwise the header is empty

3. The Images field is coming up empty when exported into csv file

This because there is need to have a special field formatter to format image into url. Module image_url_formatter does exactly that

4. Price is not importing accordingly all zeros

-ensure price is of type “raw amount”(i.e 8000 for $80.00) in the exported CSV file

5. Product Status is always importing into ‘Disable’ State.

Ensure the exported value of product status is in format of ‘1/0’ that can be configured in the view under ‘output format’

6. Images are not displayed in Admin UI.

Ensure that the exporter is set to export image fields into ‘URI path’ output format that is provided by a separate module ‘image_url_formatter’

7. “A product with SKU some_sku could not be found. Please check that the product exists or import it first”(resulting on display only pointing to first product but not all)

Answer: ??? I am not sure…still figuring this out

8. ‘PDOException: SQLSTATE[42S02]: Base table or view not found:’

Make sure cache is cleared after view created/updated. Also ensure your view returns results

9. Missing Feeds plugin FeedsCommerceProductProcessor

This error come up when there was missing ‘commerce_feeds’ module. After installing and enabling it, the error goes away

10. User by id ‘864’…

This happens when running importer with user configured that doesn’t exist on current Drupal instance. Go to admin/structure/feeds/NAME_OF_YOUR_IMPOTER/settings/FeedsNodeProcessor and update user with any user currently present

11. Warning: is_dir(): Unable to find the wrapper “private” – did you forget to enable it when you configured PHP? in file_prepare_directory()

Ensure that the private dir is configured for your Drupal instance (see “Private file system path” at admin/config/media/file-system)

12. Target is missing for Node Processor of Importer

Make sure the field is present fro the content type you are importing

13. Exports exactly 10 items only no matter what the pager is set to

This is happening when the view exported is not of type ‘Data Export’. Ensure exporting view is of type “Data Export”

Reference

  • https://drupal.org/project/commerce_feeds
  • http://drupal.stackexchange.com/questions/87039/how-can-i-import-and-export-commerce-products-in-drupal-7
  • https://drupal.org/node/622698
  • https://drupal.org/project/commerce_feedsmulti
  • http://www.drupalcommerce.org/node/467

Making It Permanent for File_Managed

The file_managed functionality comes with ajax capability to upload and remove file, however. While, it stores the file, it stores it temporarily,so unless we make it permanent, the files just uploaded will be removed after certain time. In this post, we demonstrate one of the many ways how to make the file upload via file_managed a permanent.

Hook Into File_Managed

First, we specify the callback for the form element ‘file_managed’ as following:

$form['backgrounds']['ds_theme_custom_background'] = array(
        '#title' => t('Custom Background'),
        '#type' => 'managed_file',
        '#description' => t('Upload a your custom background image, allowed extensions: jpg, jpeg, png, gif'),
        '#default_value' => isset($form_state['value']['ds_theme_custom_background']) ? $form_state['value']['ds_theme_custom_background'] : theme_get_setting('ds_theme_custom_background'),
        '#upload_location' => DESIGNSSQUARE_THEME.'/theme-settings',
        '#upload_validators' => array(
            'file_validate_extensions' => array('jpg jpeg png gif'),
            // Pass the maximum file size in bytes
            'file_validate_size' => array(MAX_SIZE_LIMIT_DS_THEME*1024*1024),
        ),
        '#process' => array('our_custom_callback'),
    );
}

By specifying ‘#process’, the custom callback – our_custom_callback is being called when ‘upload’, ‘remove’ button is clicked

Make it Permanent

Next, we will ensure file is stored permanently by updating it status as following:

function our_custom_callback($element, &$form_state, $form){
    $element = file_managed_file_process($element, $form_state, $form);

    //file upload
    if (isset($form_state['input']['_triggering_element_name']) && $form_state['input']['_triggering_element_name'] == $element['upload_button']['#name'] && !empty($element['#file'])) {
        //make file permanent
        $file = $element['#file'];
        // Change status to permanent.
        $file->status = FILE_STATUS_PERMANENT;

        //all permanent files need an entry in the 'file_usage' table
        //we also add variable name as the 'type' parameter, so we can export via Sample Data module
        file_usage_add($file, 'designssquare_lib', $element['#name'], '1');
        // Save.
        file_save($file);
    }

    //file removed
    if (isset($form_state['input']['_triggering_element_name']) && $form_state['input']['_triggering_element_name'] == $element['remove_button']['#name']) {
        //ensure file is removed
        if(isset($element['#file']->fid) && $file = file_load($element['#file']->fid)){
            //file exist, lets remove
            //the file is removed despite the count of references present
            db_delete('file_managed')->condition('fid', $file->fid)->execute();
            db_delete('file_usage')->condition('fid', $file->fid)->execute();
            entity_get_controller('file')->resetCache();
        }
    }

    return $element;
}

Here, we check whichever button is clicked – ‘upload’ or ‘remove’. If it is ‘upload’, then we change the status of the file and add entry in the table ‘file_usage’ both needed to make file permanently stored. If button ‘remove’ is clicked, then we ensure the file is removed from both tables – file_managed as well as file_usage. The last step shouldn’t be needed, however. It happens that file_managed functionality, sometimes, adds more than one reference to the same file into file_manage table(see ‘count’) resulting into situation that it doesn’t remove the file because it sees more than one reference. So, we ensure the file is removed no matter how many references listed in the table.

Troubleshooting

1. Fatal error: Call to undefined function – our_custom_callback

For Drupal 2.26,2.28 and, probably, many other version, there seems to be a bug on how the form processing functions are called before all the dependencies are loaded resulting in the error “Fatal error: Call to undefined function – our_custom_callback”. To avoid it, we end up hooking into the Drupal bootstrap process to load our custom function before callback is called via hook_init() as following:

function MODULE-NAME_init(){
    //load for processing file_managed on Ajax calls(i.e. upload, remove)
    module_load_include('inc', 'MODULE-NAME', 'inc/file_defining_custom_callback');
}

Here, we load the file containing the definition of our callback – our_custom_callback. Since the hook_init is called before Drupal is executed, our custom callback is defined that way solving the above problem.

Working with Menus for Themeing in Drupal

In this post, first we look at how to render menu in the template. Afterwards, we look how to manipulate menu html layout and its attributes so the menu can be themed per your design. At last, we go over on having custom menu that is customized in similar way via hooks

1. Build Menu

We like to have menus in the Html scope(THEME_html_process) instead the page scope(THEME_page_process). This is because menu are shared across the page layouts in general, so it is not specific to page per say, however. Drupal comes with the menu already available in the page scope under ‘main-menu’. So, if we want the menu to be available in the html scope we have to create one:

function THEME_preprocess_html(&$vars){{
...
        // Primary nav build links.
        $vars['primary_nav'] = menu_tree(variable_get('menu_main_links_source', 'main-menu'));
}

This builds the render array of main menu and make it available in the html scope. To render the menu itself in html.tpl.php, we would:

    <?php print render($primary_nav); ?>

This take array of main menu generated in preprocess function and renders into html to display in page

2. Overwrite UL Element

Next we overwrite the default ul element and its classes with our custom via hook_menu_tree() as following:

function THEME_menu_tree(&$variables) {
    return '<ul class="nav nav-justified">' . $variables['tree'] . '</ul>';
}
3. Overwrite LI Element

At last, lets overwrite the menu elements themselves. To do so, we use hook_menu_link() as following:

function THEME_NAME_menu_link(array $variables) {
      $element = $variables['element'];
    $sub_menu = '';

    if ($element['#below']) {
        // Prevent dropdown functions from being added to management menu so it
        // does not affect the navbar module.
        if (($element['#original_link']['menu_name'] == 'management') && (module_exists('navbar'))) {
            $sub_menu = drupal_render($element['#below']);
        }
        else if ((!empty($element['#original_link']['depth'])) && ($element['#original_link']['depth'] == 1)) {
            // Add our own wrapper.
            unset($element['#below']['#theme_wrappers']);
            $sub_menu = '<ul class="dropdown-menu">' . drupal_render($element['#below']) . '</ul>';
            // Generate as standard dropdown.
//            $element['#title'] .= ' <span class="caret"></span>';
            $element['#attributes']['class'][] = 'dropdown';
            $element['#localized_options']['html'] = TRUE;

            // Set dropdown trigger element to # to prevent inadvertant page loading
            // when a submenu link is clicked.
            $element['#localized_options']['attributes']['data-target'] = '#';
            $element['#localized_options']['attributes']['class'][] = 'dropdown-toggle';
            $element['#localized_options']['attributes']['data-toggle'] = 'dropdown';
        }
    }

    $element['#localized_options']['attributes']['class'][] = 'agri-nav-item';
    $element['#localized_options']['attributes']['class'][] = 'nav-space';

    //set the parent active when child is currently selected
    if(in_array("active-trail", $element['#attributes']['class'])){
        $element['#attributes']['class'][] = 'active';
        $element['#attributes']['class'][] = 'open';
    }
    // On primary navigation menu, class 'active' is not set on active menu item.
    // @see https://drupal.org/node/1896674
    if (($element['#href'] == $_GET['q'] || ($element['#href'] == '<front>' && drupal_is_front_page())) && (empty($element['#localized_options']['language']))) {
        $element['#attributes']['class'][] = 'active';
    }
    $output = l($element['#title'], $element['#href'], $element['#localized_options']);
    return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}

Here, we overriding everything from boostrap_menu_link() and made few changes. Added extra classes to the link element, removed caret and ensure parent of selected element is highlighted.

What If You Have Another Menu?

Say you have another menu at the bottom with different markup and class attributes. This menu id is ‘bottom-menu’

A)Build Menu

Build custom menu similar to the Main menu as following:

function THEME_preprocess_html(&$vars){
...
        $vars['bottom_nav'] = menu_tree('menu-bottom-menu');
        $vars['bottom_nav']['#theme_wrappers'] = array('menu_tree__bottom');
}

In the first line, the menu is build via menu_tree() function that takes the menu id and builds the array to render. In the second line, we specify hook for overriding UL element. There is one by default(hook_menu_tree__menu_MENU_NAME), so instead of overriding, we may have used the default one(i.e. hook_menu_tree__menu_bottom_menu)

B) Overwrite UL Element

With the override function specified, we can overwrite the UL element:

function THEME-NAME_menu_tree__bottom(&$variables) {
    return '<ul class="nav-agri nav-tabs-agri nav-justified-agri">' . $variables['tree'] . '</ul>';
} 

Here we use the hook specified, however. You may as well used the default hook(i.e. hook_menu_tree__menu_bottom_menu)

C)Overwrite LI Element

By default the override hook for LI element is hook_menu_link__MENU_ID() plus the menu id. In our case, the override function hook is hook_menu_link__menu_bottom_menu. So, we overwrite the links as following:

function THEME-NAME_menu_link__menu_bottom_menu(array $variables) {
    $element = $variables['element'];
    $output = l($element['#title'], $element['#href'], $element['#localized_options']);
    return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . "</li>\n";
}

There is no dropdown sub-menu to worry, so we create a link and wrap into the LI element before returning

A Word or Two on User Menu

Its very likely your main menu is different than the user menu, so how to handle the html layout for user menu while main menu is using the default hooks for structuring menus
1. User Tree Menu
The user menu has a custom hook – THEME_menu_tree__user_menu available to structure menu tree as following:

function THEME_menu_tree__user_menu(&$variables){
    return '<ul class="dropdown-menu">'.$variables['tree'].'</ul>';
}

2.User Menu Links
There is custom menuhook – THEME_menu_link__user_menu available to structure the menu item list

3.Menu Template file
There is also custom template file – block–system–user-menu.tpl available for you to override block html as you wish for user menu as well

Adding CKEditor Plugins Manually in Drupal

CKEditor has a lot of new good plugins that doesn’t have supporting Drupal module for easy integration. In this post, we go over how to integrate any native CKEditor plugin without supporting Drupal module present. Specifically, we cover how to install the plugin as part of CKEditor module and how to do the same but as separate custom module. In this post, we have CKEditor installed as a stand alone module not part of Drupals WYSIWYG install. How to install stand alone editor such as CKEditor, please, see post Wysiwyg With Ckeditor In Drupal

To make it more practical, lets install CKEditor plugin – Leaflet that adds map functionality to the editor

Add Plugin Part of CKEditor Module

1.) Download and unzip the plugins here (depending on your setup):

“sites/all/modules/ckeditor/plugins” or “sites/all/modules/contrib/ckeditor/plugins”.

After that, you should have the following folder structures:
“ckeditor/plugins/leaflet”
“ckeditor/plugins/lineutils”
“ckeditor/plugins/widget”

2.) We should now activate the new plugins and add them to the toolbar.

This is done by configuring the Ckeditor Profile Settings, in which by default is located in:
“admin/config/content/ckeditor/edit/Advanced”
“admin/config/content/ckeditor/edit/Full”

A. Activate the Plugins
In EDITOR APPEARANCE >>> Plugins section:
Enable the corresponding checkboxes for the Leaflet, Line Utilities, and Widget plugins.
These are the texts displayed adjacent to their checkboxes:
“Plugin file: leaflet”
“Plugin file: lineutils”
“Plugin file: widget”

B. Add them to the Toolbar
We should make the activated plugins visible in the toolbar,
skipping this step will make the Leaflet plugin inaccessible in the toolbar.

In EDITOR APPEARANCE >>> Toolbar section:
Drag the Leaflet Maps icon (black-colored) from the ‘All Buttons’ section to the ‘Used Buttons’ section.

We need to configure the Leaflet Maps icon only since the Line Utilities and Widget plugins
have no toolbar icons and they will just load in the background.

3.) Then, click the Save button. Clear the Drupal’s overall cache AND clear the browser’s cache.

Clearing the browser’s cache is also very important since the CKEditor’s JS and CSS assets/components are cached also in the browser.

As indicated above, using the “sites/all/modules/ckeditor/plugins” or “sites/all/modules/contrib/ckeditor/plugins”
will work with no additional custom hooks programming since by default CKEditor utilize that folder
in the CKEditor Global Profile Settings: “admin/config/content/ckeditor/editg”.

Adding Plugins As Stand Alone Module

The above approach requires the plugins to be part of the CKEditor Module, but it may not be desirable especially in case for distributing your custom module that requires one of the CKEditor plugin. In that case, its good idea to keep CKEditor plugins in stand alone module part of your custom module perhaps. Here is how to do so:

Lets say your custom module name is ‘custom-module’ and you are going to keep CKEditor plugins in sub-folder ‘ckeditor-plugins’, then the destination location would be:
“sites/all/modules/custom-module/ckeditor-plugins”

After that, you should have the following folder structures:
“sites/all/modules/custom-module/ckeditor-plugins/leaflet”
“sites/all/modules/custom-module/ckeditor-plugins/lineutils”
“sites/all/modules/custom-module/ckeditor-plugins/widget”

2.) Hook Plugins Into CKEditor

Next, lets hook the plugins into CKEditor via hook_ckeditor_plugin as following in custom-module.module file:

/********CKEditor plugins********/
function custom-module_ckeditor_plugin()
{
    $module_path = drupal_get_path('module', 'custom-module');
    $plugins     = array();

    $plugins['leaflet'] = array(
        'name'    => 'leaflet',
        'desc'    => t('DesignsSquare.com: CKEditor: Leaflet plugin'),
        'path'    => $module_path . '/ckeditor-plugins/leaflet/',
        'default' => 't',
        'buttons' => array(
            'leaflet' => array(
                'label' => 'Leaflet Map',
                'icon'  => 'icons/leaflet.png',
            ),
        ),
    );

    $plugins['lineutils'] = array(
        'name'    => 'lineutils',
        'desc'    => t('DesignsSquare.com: CKEditor: LineUtils plugin'),
        'path'    => $module_path . '/ckeditor-plugins/lineutils/',
        'default' => 't'
    );

    $plugins['widget'] = array(
        'name'    => 'widget',
        'desc'    => t('DesignsSquare.com: CKEditor: Widget plugin'),
        'path'    => $module_path . '/ckeditor-plugins/widget/',
        'default' => 't'
    );

    return $plugins;
}

Here, the variable ‘name’ is as specified in the plugin.js that is part of all CKEditor Plugins (look for – CKEDITOR.plugins.add(“NAME”‘) – in the plugin.js for each plugin). The button index(highlight line) has to be exactly as specified in the plugin.js(look for ‘ui.addButton(“BUTTON-NAME”). The variable ‘icon’ specifies the location for the image of the button. Ensure it is there.

Afterwards, the plugins will show up in the CKEditor Configuration section(admin/config/content/ckeditor/edit/Full(or Advance). The rest is the same from the first approach where you would check the plugins in ‘Plugins’ section at admin/config/content/ckeditor/edit/Full(or Advance) and then move the icon into Toolbar.

Troubleshooting

1. Unable To Add Icons in Toolbar

Turn off the toolbar and add icons manually. To turn off the toolbar go to the admin/config/content/ckeditor/editg and select ‘disable’ for ‘Use toolbar Drag&Drop feature’. I am guessing that the toolbar misbehaves because CKEditor Module imports JQuery UI sortable lib that is already imported once by the core. As result, it causes js fatal errors. I am out of time to verify.

2. Uncaught TypeError: Cannot read property ‘icons’ of null

Ensure the name of the plugin as specified in the plugin.js(i.e. CKEDITOR.plugins.add(‘Here-comes-name’,) is the same used in the hook_ckeditor_plugin(). Set a break point with condition of ‘s === null’ where s is the variable the attribute ‘icons’ is called on(i.e s.icons) to help identified which plugin is causing issue in ckeditor.js

References:

  • http://docs.cksource.com/CKEditor_for_Drupal/Enterprise/Drupal_7/Plugins
  • https://github.com/ranelpadon/ckeditor-leaflet/blob/master/Installation%20Guide.txt

Solving Menu Import Issue when Featuring Sample Data in Drupal

[UPDATE] – alias menu import module was rolled into “Sample Data” module that is of newer version. it is listed on Drupal site
Here is post about it – http://margotskapacs.com/2014/07/importexport-sample-data-and-assets-for-kickstart-all-via-features/

Currently, if you would like to export and import the sample data( like kickstart) via features, then the menus for your data breaks because it uses the hard coded path(i.e. node/id) and the new instance has different path for the nodes imported. To my understanding there is a work in place to fix this in future release of features by using the path uuid but of January, 2014 that is not available at least in stable version

The solution presented here utilizes ‘pathauto’ and its generated alias path for each node to do the mapping between the same nodes on two different Drupal instances. In another words, we export existing menu structure with alias path as reference. Once these nodes are imported(before menu import script is run) on the another Drupal instance, it automatically generates the default alias path(for custom alias, please, section ‘If Custom Path Alias…’ below). Since these alias path are the same between the drupal instances, the installation script looks up the mapping when structuring and building menu.

To maintain the existing menu structure, you will have to run a script that will generate the structure used by the menu importer script as showed in this post. This may be improved in future but author didn’t have time at this time and may not be a value since there is path uuid project in progress that will be the better solution in long run

In addition, the path for nodes has to be unique as well.

Requirement:

  1. Pathauto module has to be installed and enabled
  2. Each node have to have path alias or setting ‘Generate automatic URL alias’ selected under ‘Url path settings'(this is default behavior once ‘pathauto’ enabled, so you should need to do anything unless you have nodes present and you just installed/enabled ‘pathauto’. In that case, you have to go into each node and check the setting ‘Generate automatic URL alias’)
  3. The nodes has to be already imported before running the menu_import on the new Drupal instance
  4. (Recommended) When importing Nodes via features, ensure the StrongArm variable or configuration of ‘pathauto_node_pattern’ is also imported. This ensures that for imported nodes the generated alias path is the same as the one in current Drupal instance. This is especially important if the alias pattern for nodes is not the default one

The solution consists of two parts:

Part I: Build Exportable Menu

To build current menu structure for menu of your choice run the following script in “PHP Execute” block on Drupal instance you want to export the menu from(copy/past and then specify menus to export on line 3):

include_once DRUPAL_ROOT . '/includes/utility.inc';
//specify menues to export
$menus = array('menu-bottom-menu', 'main-menu');
$mainMenuArray = array();
//mlid to alias map
$alias_map = array();
$menu_items = array();

$output = "function _menus_altered(){ \n";
$output .= "return array (\n";

foreach ($menus as $menu) {
    $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
        ->fields('ml')
        ->condition('ml.menu_name', $menu)
        ->condition('ml.hidden', '0')
        ->orderBy('weight')
        ->execute()
        ->fetchAll();

    foreach ($links as $key => $link) {
        $links[$key]['options'] = unserialize($link['options']);
    }
    $mainMenuArray = array_merge($mainMenuArray, $links);

    $mlid_ins = db_select('menu_links', 'ml')
        ->fields('ml', array('mlid', 'link_path'))
        ->condition('ml.menu_name', $menu)
        ->condition('ml.hidden', '0')
        ->execute()->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);

    $menu_items = $menu_items + $mlid_ins;

    $menu_ins = menu_load($menu);
    if ($menu_ins) {
        $menu_item = 'array(';
        $menu_item .= "'menu_name' => '" . $menu_ins['menu_name'] . "',";
        $menu_item .= "'title' => '" . str_replace("'", "\'", $menu_ins['title']) . "',";
        $menu_item .= "'description' => '" . str_replace("'", "\'", $menu_ins['description']) . "'";
        $menu_item .= ")";
        $output .= $menu_item . ",\n";
    } else {
        watchdog(WATCHDOG_NOTICE, "menu: $menu not found");
    }
}
$output .= "    );\n";
$output .= "}\n\n\n";

//generate the map between mlid and alias path
foreach ($menu_items as $mlid => $options) {
    $alias_map[$mlid] = drupal_get_path_alias($options['link_path']);
}
$output .= "function _menu_installed_items() {\n";
$output .= "return array (\n";
foreach ($mainMenuArray as $key => $link) {
    $alias_path = drupal_get_path_alias($link['link_path']);

//prepare link to pass in menu_link_save()
    $link_export = $link;
    $link_export['link_path'] = $alias_path;
    unset($link_export['mlid']);
    unset($link_export['router_path']);
    unset($link_export['options']['identifier']);
    $link_export['plid'] = ($link_export['plid']) ? $alias_map[$link_export['plid']] : $link_export['plid'];
    $link_export['p1'] = ($link_export['p1']) ? $alias_map[$link_export['p1']] : $link_export['p1'];
    $link_export['p2'] = ($link_export['p2']) ? $alias_map[$link_export['p2']] : $link_export['p2'];
    $link_export['p3'] = ($link_export['p3']) ? $alias_map[$link_export['p3']] : $link_export['p3'];

    $output .= "'" . $alias_path . "' => " . drupal_var_export($link_export) . ",\n";
}
$output .= ");\n";
$output .= "}\n";

drupal_set_message("<textarea rows=100 style=\"width: 100%;\">" . $output . '</textarea>');

First copy and past the above code in to “PHP Execute” block. Second, specify the menus you like to export in the line 3 variable ‘$menus’
This script generates two arrays encapsulated in functions _menu_installed_items() and _menus_altered() that you will have to supply in your custom module installation file in next step

Part – II : Build Custom Module to Import Menus

Create a custom block with the following .install script:

function MODULE_NAME_install(){
    $t = get_t();

   // clear menus and create new ones if doesn't exist
    foreach(_menus_altered() as $menu){
        //remove menu links for clean install
        menu_delete_links($menu);
        drupal_set_message($t('Links deleted from menu - ' . $menu['menu_name']));

       if(!menu_load($menu['menu_name'])){
           //doesn't exist...lets create one
           drupal_set_message($t('Missing menu '.$menu['menu_name'].'...creating'));
           menu_save($menu);
       }
    }

    //map containing the structure of the menu
    $alias_map = array();

    //shuffle so that parents are always above the children menu items
    //this is important so when building the $alias_map child is not being inserted without parent already present
    $all_items = _menu_installed_items();
    $parents_p1 = array();
    $parents_p2 = array();
    $parents_p3 = array();
    $parents_p4 = array();
    $parents_p5 = array();

    foreach($all_items as $key => $item){
        if($key == $item['p1']){
            $parents_p1[$key] = $item;
        }
        if($key == $item['p2']){
            $parents_p2[$key] = $item;
        }
        if($key == $item['p3']){
            $parents_p3[$key] = $item;
        }
        if($key == $item['p4']){
            $parents_p4[$key] = $item;
        }
        if($key == $item['p5']){
            $parents_p5[$key] = $item;
        }
    }

    $sorted_menu_list = array_merge($parents_p1,$parents_p2,$parents_p3,$parents_p4,$parents_p5);

    //add links
    foreach ($sorted_menu_list as $key => $item) {
        $alias_link_path = $item['link_path'];
        //look up node based on the alias path of existing drupal instance
        $item['link_path'] = drupal_get_normal_path($alias_link_path);

        //drupal_set_message('plid: '.$item['plid']. ' alias_map:' . $alias_map[$item['plid']] );
        $item['plid'] = ($item['plid'] !== 0 && !empty($alias_map[$item['plid']])) ? $alias_map[$item['plid']] : $item['plid'] ;
        $item['p1'] = ($item['p1'] !== 0 && !empty($alias_map[$item['p1']])) ? $alias_map[$item['p1']] : $item['p1'] ;
        $item['p2'] = ($item['p2'] !== 0 && !empty($alias_map[$item['p2']]) ) ? $alias_map[$item['p2']] : $item['p2'] ;
        $item['p3'] = ($item['p3'] !== 0 && !empty($alias_map[$item['p3']])) ? $alias_map[$item['p3']] : $item['p3'] ;

        $installed = menu_link_save($item);
        if($installed !== FALSE) {
            drupal_set_message($t('Menu Item : '. $key . ' installed'));
            //drupal_set_message('INSERT: link_path: '.$alias_link_path. ' mlip:' . $installed );
            $alias_map[$alias_link_path] = $installed;
        }else{
            drupal_set_message($t('Menu Item : '. $key . ' was not installed'));
        }
    }
    menu_link_save($item);
    menu_cache_clear_all();
    drupal_set_message($t('Installed Menu'));
}

//ADD THE TWO FUNCTIONS FROM PART I HERE

Make sure you add the two functions generated in Part I (i.e._menus_altered(),_menu_installed_items() ) in the .install file where it says ‘//ADD THE TWO FUNCTIONS FROM PART I HERE’. These are used by the script to build the new menus

Thats all….in future we hope to automize the above process by creating module if the problem is not solved soon by the features,uuid modules.

If Custom Path Alias…

The features module currently does not export/import path_alias for nodes. This is okey if you are using path alias of the node that were generated by default by pathauto module, however. If not, then your custom alias path for nodes are not exported/imported via features and the above menu export/import solution will not work. We have created module to export/import path alias published at https://github.com/kapasoft-config-scripts/designssquare_alias_path. By enabling this module, your custom path alias is going to be exported and then imported for all nodes.

Troubleshooting

All of the menu items does not import

Make sure all the menu items imported are pointing to path alias that are unique. If not unique, then you have to break into more than one menu import to work.

Handling Assets for Custom Module in Drupal

[UPDATE Aug 22, 2014]There is newer version of this article “Import Export Sample Data And Assets For Kickstart All Via Features” and a Drupal module samle_data at Drupal site. It is also in the git repo DesignsSquare Lib Sample Data at branch ‘7.x-1.x’

You have custom module a plugin that you have made exportable via Features module. This custom module has sample kickstart data that contains assets such as images, video, etc. In this post, we cover how to manage these assets, so when user enables your custom module, those are copied accordingly and automatically. Furthermore, we provide a custom module on github to handle assets for you so you don’t have to ever worry about assets import/export for you sample kickstart data. At last, the assets referenced directly from content is also packaged for auto transfer whenever a custom module is enabled.

Env: DRupal:7.26, Features:7.x-2.0, UUID:7.x-1.0-alpha5+17-dev, Feature_UUID:7.x-1.0-alpha3+15-dev

About uuid_features_file_path

Each artifact exported via Features module receives an attribute – uuid_features_file_path. This attribute specifies where are the artifact located which is used at the time of import(when module is enabled). We are going to set this attribute at the time of export, so it is pointing to our artifact location. Its important to note, we like to keep our assets part of module dir that way all plugin info is in one dir easier to deliver and import for user.

Set uuid_features_file_path At Export

We are going to hook into feature export process via MODULE-NAME_uuid_node_features_export_render_alter(&$export, $node, $module) as following:

function MODULE-NAME_uuid_node_features_export_render_alter(&$export, $node, $module){
    if($module == 'MODULE-NAME'){
        //setting artifact source location so it imports accordingly
        $field_instance = field_get_items('node', $node, 'field_ARTIFACT-FIELD-NAME');
        $export->field_ARTIFACT-FIELD-NAME['und'][0]['uuid_features_file_path'] = drupal_get_path('module','MODULE-NAME').'/imports/'.$field_instance[0]['filename'];
...
}

Here, we first ensure the code is executed for our module by having IF condition. Afterward, we retrieve field containing artifact for particular Node instance. Next, we update the export attribute uuid_features_file_path with a path to the custom module and path to artifact

This will export the custom module sample date into file with uuid_features_file_path specifying assets located in our custom module folder. There is nothing else to do, because at the import, features module will take care of coping the assets from location specified in uuid_features_file_path to the public dir of that Drupal instance in folder assets “uri” is set to

Forget About Assets…

In the above solution, a user is constrained to have the module placed in the exact location and named accordingly as specified in the uuid_features_file_path, otherwise, it will break. How about forgetting about assets of the sample kickstart data, so you can focus building features. Here is a Drupal module that does just that:
https://github.com/kapasoft-config-scripts/designssquare-lib-assets

Once enabled, this module does the following:

  • Handles all assets of the sample data being exported/imported via Features module
  • Besides Drupal core fields, it also works for custom fields referencing assets
  • It stores the assets relative to the module, so user can name the exportable feature or place it as they wish
  • The assets need to be handled by ‘file_managed’ functionality in order for the module to work

With this module enabled, you should never need to worry about assets any more.

How About Assets Referenced From Content …In Sample Data

So far, we have handled asset export/import for cases where the asset is referenced by some field in a node, however. Most of the time, your sample data will have content that will also reference assets(i.e.

<img src="/sites/default/files/img/some.jpg" alt="thumbnail">

). We also like to export/import those assets, so the sample data doesn’t break on clients Drupal instance. Here are steps to accomplish this

1. Make Part of Custom Module

Make sure the assets is part of the custom module directory…let, say the assets are in ‘modules/CUSTOM-MODULE/assets/img’ directory

2. Generate Array of Assets To Import

Run the following script in the ‘PHP Executable’ block to generate an array of assets to export/import:

include_once DRUPAL_ROOT . '/includes/utility.inc';
include_once DRUPAL_ROOT . '/includes/file.inc';

$MODULE_NAME = 'CUSTOM-MODULE-NAME';
$PATH_TO_MODULE = drupal_get_path('module', $MODULE_NAME);
//source dir or location of the files to import relative to the module specified by variable - $MODULE_NAME
$BASE_SOURCE_REL_PATH = '/assets/img';
//the destination folder relative to the public dir of Drupal instance
$DESTINATION_DIR = '/img';

$output = "function _assets_list(){ \n";
$output .= "\$module_dir = drupal_get_path('module', '" . $MODULE_NAME . "');\n";
$output .= "     return array (\n";

$output .= write_file_array('', $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR);

$output .= "     );\n";
$output .= "}\n";
drupal_set_message("<textarea rows=100 style=\"width: 100%;\">" . $output . '</textarea>');

function write_file_array($rel_path = '', $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR)
{

    $output = '';
    $full_path = $PATH_TO_MODULE . $BASE_SOURCE_REL_PATH . $rel_path;
    if ($handle = opendir($full_path)) {
        while (false !== ($file = readdir($handle))) {
            if (is_dir($full_path . '/' . $file) && $file != '.' && $file != '..') {
                $output .= write_file_array('/' . $file, $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR);
            } elseif ($file != '.' && $file != '..') {
                $uri = file_build_uri($DESTINATION_DIR . $rel_path . '/' . $file);
                $output .= "     '" . $uri . "' => array(\n";
                $output .= "     'uri' => '" . $uri . "',\n";
                $output .= "     'filename' => '" . $file . "',\n";
                $output .= "     'source' => \$module_dir.'" . $BASE_SOURCE_REL_PATH . $rel_path . '/' . $file . "',\n";
                $output .= "),\n";
            }
        }
        closedir($handle);
    }
    return $output;
}

In line 4, we specify our custom module that contains the assets to export. In line 7, we specify the exact directory of assets to export relative to our custom module. In line 9, we specify the destination folder to import the assets relative to the public directory of the Drupal instance. In another words, if the sample content references image via tag

<img src="/sites/default/files/img/some.jpg" alt="thumbnail">

then the destination dir is ‘/img’ assuming the Drupal public dir is ‘sites/default/files’. After running this script, it will generate function ‘_assets_list()’ that contains all the assets to export/import and is going to be used part of the installation script as described next step

3. Transfer assets via module installation script

Next, we configure our Custom Module installation script CUSTOM-MODULE-NAME.install to transfer assets whenever the module is enabled

function CUSTOM-MODULE-NAME_install(){
    $t = get_t();

    //looping through the files and coping to the destination
    foreach(_assets_list() as $file){
        $uri = $file['uri'];
        $dir_name = drupal_dirname($uri);
        file_prepare_directory($dir_name,FILE_CREATE_DIRECTORY);
        (!file_exists(drupal_realpath($uri))) ? file_unmanaged_copy($file['source'], $uri) : '';
    }
    drupal_set_message($t('CUSTOME MODULE NAME assets transferred'));
}

/************COPY _assets_list() HERE*******/

NOTE: Please, copy the function _assets_lists() from the step 2 in the same file CUSTOM-MODULE-NAME.install

This script is run when custom module is enabled. It runs through each asset and transfers from the CUSTOM-MODULE/img into the Drupal/public/img directory. Afterwards, those assets are available when referenced from the sample content

4. Clean on Uninstall

You may also like to remove these assets whenever the custom module with sample data is uninstalled, then add the following in CUSTOM-MODULE-NAME_uninstall() in the CUSTOM-MODULE-NAME.install file:

function CUSTOM-MODULE-NAME_uninstall(){
    $t = get_t();

    //looping through the files and removing one by one
    foreach(_assets_list() as $file){
        $uri = $file['uri'];
        $filename = drupal_realpath($uri);
        (file_exists($uri)) ? file_unmanaged_delete($filename) : '';
    }
    drupal_set_message($t('CUSTOM-MODULE-NAME assets removed'));
}

This script is run on module uninstall and it runs through the same list of assets from _assets_list() and removes each asset from the public dir

Summarize

The attribute uuid_features_file_path specifies the location from where to transfer assets at the time of import, so we hooked into the export and updated this attribute to a location of our assets. We keep it in the same custom module dir for easy delivery and import by user. Furthermore, there is a module developed in github that will handle all assets of the sample data kickstart for you. At last, we also go over how to export/import assets referenced by sample data from the content itself

References

http://drupalcontrib.org/api/drupal/contributions!features!features.api.php/7

How To Implement Custom Field Types In Drupal

In this post, we go over on how to create, handle and display a custom fields in Drupal. In addition, we look on situations when your custom field contains an artifacts that requires an additional care

The hook_field_schema() defines the columns. The table will be only added after an actual field is created(attached to content type). So, if you like to see if the data is being added for the custom field, look into table ‘field_xxx’

The hook_field_shema() needs to be define and declared in the module .install file as following:

function NAME-OF-SCHEMA_field_schema($field) {
    $schema = array();
    $schema['columns']['class'] = array(
        'type' => 'varchar',
        'length' => 50,
        'not null' => FALSE
    );
  
    $schema['columns']['image'] = array(
        'description' => 'The {file_managed}.fid being referenced in this field.',
        'type' => 'int',
        'not null' => FALSE,
        'unsigned' => TRUE,
    );

    $schema['foreign keys'] = array(
        'image_fid' => array(
            'table' => 'file_managed',
            'columns' => array('image' => 'fid'),
        ),
    );

    return $schema;
}

In the above example, a schema of 2 fields are created. One of the fields are a file upload, so there is additional foreign constrain defined.

Test from PHP Execute block

$field = field_info_field('field_designssquare_slider_layer');
module_load_install('field_designssquare_slider_layer');
$schema = (array) module_invoke('field_designssquare_slider_layer', 'field_schema', $field);

Look into Watchdog logs for a message. You should see the message if the install was performed

The columns can also be checked in the table ‘field_NAME_OF_FIELD’ in database

Specify Display

You may like to create a display format for the custom field. The field display gives user ability to select display for the field. Developer can take it and create separate layout via custom theme functions wrapped into template file

To declare display:

/**
 * Implements hook_field_formatter_info().
 */
function videojs_field_formatter_info() {
  return array(
    'videojs' => array(
      'label' => t('Video.js : HTML5 Video Player'),
      'field types' => array('file', 'media', 'link_field'),
      'description' => t('Display a video file as an HTML5-compatible with Flash-fallback video player.'),
      'settings'  => array(
        'width' => NULL,
        'height' => NULL,
        'posterimage_field' => NULL,
        'posterimage_style' => NULL,
      ),
    ),
  );
}

The ‘field types’ specify to which types the display is available. The ‘settings’ is custom parameter passed to the display(will cover later). So, this declares the display available for user to specify from UI. Next, lets implement the display:

/**
 * Implements hook_field_formatter_view().
 */
function videojs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  if ($display['type'] !== 'videojs') {
    return array();
  }
  if (empty($items)) {
    return array();
  }

  if ($field['type'] == 'link_field') {
    //for field type of 'link_field' do things differently
  }

  $settings = $display['settings'];
  $attributes = array();
  if (!empty($settings['width']) && !empty($settings['height'])) {
    $attributes['width'] = intval($settings['width']);
    $attributes['height'] = intval($settings['height']);
  }


  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
  return array(
    array(
      '#theme' => 'videojs',
      '#items' => $items,
      '#player_id' => 'videojs-' . $id . '-' . str_replace('_', '-', $instance['field_name']),
      '#attached' => videojs_add(FALSE),
      '#entity' => $entity,
      '#entity_type' => $entity_type,
      '#attributes' => $attributes,
      '#posterimage_style' => !empty($settings['posterimage_style']) ? $settings['posterimage_style'] : NULL,
    ),
  );
}

The display formatter calls a custom function declared as following:

function videojs_theme() {
  return array(
    'videojs' => array(
      'variables' => array('items' => NULL, 'player_id' => NULL, 'attributes' => NULL, 'entity' => NULL, 'entity_type' => NULL, 'posterimage_style' => NULL),
      'template' => 'theme/videojs',
    ),
}

Here, the theme custom function used for rendering display is declared. This theme custom function uses a template file ‘theme/videojs’ to render output, but before that the hook_preprocess_CUSTOM-THEME-FUNC is called, so the variables can be preprocessed.

Render the Display

To render specified display as following:

$video_render_array = field_view_field(
                        'node',
                        $entity,
                        'field_video_file',
                        array(
                            'type' => 'videojs',
                            'label' => 'hidden',
                            'settings' => array(
                                  'width' => '300',
                                  'height' => '400',
                                  'posterimage_field' => NULL,
                                  'posterimage_style' => NULL,
                            ),
                        )
                    );
 $content = render($video_render_array);

Here the field ‘field_video_file’ uses a display ‘jw_player’ that takes one parameter jwplayer_preset. Afterwards, the renderable array returned from the display is being rendered.

Create Custom Field Type

To declare custom field type use hook_field_info() as following:

function MODULE-NAME_field_info() {
 return array(
        'OUR_CUSTOM_FIELD_NAME' => array(
            'label' => t('Slide Layer'),
            'description' => t('field containing info about one of the layers part of the Rev Slider'),
            'settings' => array('max_length' => 255),
            'instance_settings' => array(
                'text_processing' => 0,
            ),
            'default_widget' => 'OUR_CUSTOM_FIELD_NAME_widget',
            'default_formatter' => 'OUR_CUSTOM_FIELD_NAME_formatter',
        ),
    );
}

Besides name and description, the default widget and formatter is specified. Next, lets declare and implement the widget for this field type :

/**
 * Implements hook_field_widget_info().
 * Expose Field API widget types.
 */
function MODULE-NAME_field_widget_info() {
    return array(
        'OUR_CUSTOM_WIdGET_NAME' => array(
            'label' => t('Revolution Slide Layer'),
            'field types' => array('OUR_CUSTOM_FIELD_NAME'),
        ),
    );
}

The widget name is given and assigned to our custom field type – OUR_CUSTOM_FIELD_NAME via ‘field types’. After declaring the widget, lets specify the form for user to enter values:

/**
 * Implements hook_field_widget_form().
 */
function MODULE-NAME_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
    $element += array(
        '#type' => $instance['widget']['type'],
        '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
    );
    return $element;
}

/**
 * Implements hook_element_info().
 * Declare the field Form API element types and specify their default values.
 */
function MODULE-NAME_element_info() {
    $elements = array();
    $elements['OUR_CUSTOM_WIdGET_NAME'] = array(
        '#input' => TRUE,
        '#process' => array('designssquare_rev_slider_layer_field_process')
    );
    return $elements;
}

In the function hook_field_widget_form(), it specifies the widget type, so we can use hook_element_info for specifying form. Afterwards, the hook_element_info() is used to specify function returning the renderable array with columns of the schema from the .install file

At last, the widget form is specified:

function designssquare_rev_slider_layer_field_process($element, $form_state, $complete_form) {
    $element['layer_header'] = array(
        'layer_header' => array(
            '#type' => 'markup',
            '#weight' => 0,
            '#markup' => '<p>' . t('Slide Layer '.($element['#delta'] + 1)) . '</p>',
        ),
    );
$element['image'] = array(
        '#title' => t('Image'),
        '#type' => 'managed_file',
        '#weight' => 8,
        '#description' => t('Upload a file, allowed extensions: jpg, jpeg, png, gif'),
        '#default_value' => isset($element['#value']['image']) ? $element['#value']['image'] : '',
        '#upload_location' => REV_SLIDER_DEST.'/img',
        '#states' => array(
            'visible' => array(
//                ':input[title="content_choice_'.$element['#delta'].'"]' => array('value' => 'image'),
                ':input[name="content_choice['.$element['#delta'].']"]' => array('value' => 'image'),
            ),
        ),
        '#upload_validators' => array(
            'file_validate_extensions' => array('jpg jpeg png gif'),
            // Pass the maximum file size in bytes
            'file_validate_size' => array(MAX_SIZE_LIMIT_DS*1024*1024),
        ),

    );
    // To prevent an extra required indicator, disable the required flag on the
    // base element since all the sub-fields are already required if desired.
    $element['#required'] = FALSE;

    return $element;
}

We still have to declare and implement hook_field_is_empty($item, $field), so it know when the field is considered to be empty

function MODULE-NAME_field_is_empty($item, $field)
{
    return !isset($item);
}

Here, we tell that empty is when no instance of our custom field is present

Validating Custom Field Values

To validate the custom field, we first register our custom validation function via hook_formt_alter():

function MODULE-NAME_form_alter(&$form, &$form_state, $form_id)
{
    switch ($form_id) {
        case 'OUR-FORM-ID':
            //assign validation
            $form['#validate'][] = 'some_custom_validate';
            //assign submition
            $form['#submit'][] = 'some_custom_submit';
            break;
    }
}

Since this is a general hook, we filter out only the form of our interest. (For finding Form ID, please, see post ‘Forms In Drupal Overview‘). Afterwards, we specify our custom function for validating the form

Next, the validation logic is declared:

function some_custom_validation(){
 if(empty($form_state['values']['some_field']['und'])){
         form_set_error('some_field[und]', t("error message comes here"));
}

On validation, our custom validation function is being called in which we perform validation for our custom field values

Handling Artifacts For Custom Field

If your custom field contains files, then there is extra care of keeping them permanent as well as removed on deletion. In the above section “Validating Custom Field Values”, we registered our submission processor function. By default all artifacts via file_manage is saved temporary. Next, we ensure the artifacts are permanent on creation and deleted on the remove event:

function some_custom_submit($form, &$form_state)
{
  
        if ((isset($form_state['clicked_button']) && $form_state['clicked_button']['#value'] == "Delete")) {
            //custom field needs to be deleted
            //remove artifact image
            $file = file_load(['values']['our_custom_field']['image']);
            if ($file) {
                file_delete($file);
                file_usage_delete($file, 'MODULE-NAME');
            }
        } else {
            //custom field needs to be added
            //make permanent image file
            $file = file_load(['values']['our_custom_field']['image']);
            if ($file) {
                // Change status to permanent.
                $file->status = FILE_STATUS_PERMANENT;
                //all permanent files needs an entry in the 'file_usage' table
                $id = (isset($form_state['node']->nid)) ? $form_state['node']->nid : 0;
                file_usage_add($file, 'MODULE-NAME', $form_state['node']->type, $id);
                // Save.
                file_save($file);
            }
        }
    }
}

First, we check to see if the node is being deleted or created. Afterwards, we load the artifact and make it permanent or remove it if being deleted

See post ‘Handling Assets For Custom Module In Drupal’ for how to automize artifact import/export

Handling Multiple Instances of Custom Field

In case, user chooses your custom field to be more than one time for specific node, then there is module field_remove_item that allows easy for them to remove any of the instance, however. You will have to handle the artifacts again. That easy to do via hook_field_validate()

function MODULE-NAME_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors)
{
    //remove artifacts  on 'remove item' bottom
            foreach ($items as $key => $item) {
               //see if any of the layers is selected to be deleted
                if ($item['field_remove_item']) {
                    //remove image
                    $file = file_load($item['image']);
                    if ($file) {
                        file_delete($file);
                        file_usage_delete($file, 'MODULE-NAME');
                    }
                }
            }
}

Here, we loop through each instance of your custom field since we don’t know exactly which of the one is being removed. The instance with field ‘field_remove_item’ set, is the one we have to remove artifacts, so we have if statement following with standard way of removing asset managed with manage_file

Uninstalling Custom Fields

If you developed a custom field and installed as a separate module than it will create a lock where you will not be able to uninstall the module. There are two solutions that i am aware. The quick and dirty is to update table ‘fields_config’ to remove the dependency. Another solution is to create a separate module for the purpose to run uninstall hook during which it removes the field, thus, removing the lock so it becomes available to be uninstalled. Lets look at the last solution

Here is the hook_uninstall() for uninstalling the custom module:

function MODULE-NAME_uninstall()
{
    //remove fields from content types
    foreach(_get_bundles_for_field_type('CUSTOM-FIELD-TYPE') as $bundle){
        field_attach_delete_bundle('node', $bundle);
        drupal_set_message('uninstalled field for bundle:'.$bundle);
    }

    //remove tables
    drupal_uninstall_schema('CUSTOM-FIELD-SCHEMA');

    // remove variables
    db_query("DELETE FROM {variable} WHERE name LIKE 'CUSTOM-FIELD-SCHEMA_%'");

    // Remove assets.
//    file_unmanaged_delete_recursive(file_default_scheme() . '://slider');

    // Clear the cache.
    field_info_cache_clear();
}

The function ‘_get_bundles_for_field_type’ returns all the bundles referencing the custom field. Afterwards, we use field_attach_delete_bundle API To remove our custom field type as well as all of its instances. Next the schema is removed, the variables deleted and files removed. At last, we clear the cache

Here is the implementation of _get_bundles_for_field_type():

//returns all the bundles of entity instances that references a field instance with a given type
function _get_bundles_for_field_type($field_type)
{
    $field_map = field_info_field_map();
    $bundles = array();
    foreach ($field_map as $field_name => $item) {
        if ($item['type'] == $field_type) {
            foreach ($item['bundles'] as $bundle_list) {
                foreach ($bundle_list as $bundle) {
                    $bundles[] = $bundle;
                }
            }
        }
    }
    return array_unique($bundles);
}

Render The Field

Troubleshooting

1. PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ‘public://some/path/img.jpg’ for key ‘uri’: INSERT INTO {file_managed}

This issue came up when our custom field was a file and by reinstalling the field it created a duplicate entry in the manage_file table. The solution was clean file_manage table when instance with our custom field was delete

//clean file_manage database on field instance delete
function MODULE-NAME_node_delete($node){
    if($node->type == 'CERTAIN-TYPE'){
        //removes images from file_manage/file_usage tables
        $field_instance = field_get_items('node', $node, 'field_FIELED-NAME');
        foreach($field_instance as $key => $instance){
            $file = file_load($instance['image']);
            file_delete($file);
            file_usage_delete($file, 'MODULE-NAME');
  ...
}

The hook-node_delete($node) is called when any node is being deleted. Here, in line 5, we look up our custom fields for the node being deleted and then clean the file entry in file_manage and file_usage tables.

This also removes the images from Drupal public directory(by default sites/default/files). We did made files being copied when module is being enabled:

function MODULE-NAME_uuid_node_features_rebuild_alter(&$node, $module){
...
            $source = _get_source_file($final_file->uri);
            file_unmanaged_copy($source, $final_file->uri);
...
}

function _get_source_file($uri){
    $source = drupal_get_path('module','MODULE-NAME').'/imports/public/'.substr($uri, 9);
    if(!file_exists($source)){
        watchdog(WATCHDOG_NOTICE, 'MODULE-NAME Import: Image does not exists at '.$source);
    }
    return $source;
}

The hook_uuid_node_features_rebuild_alter is covered in the article Make Custom Field With File Exportable In Drupal Features. We just extend it with file transfer when module feature is being enabled. In highlighted line, we copy from predefined location to a Drupal public folder.

See also post “Handling Artifacts for Custom Module In Drupal” for solution on handling assets such as images, files

2. Drupal claim “Field type(s) in use”

This message is displayed when you would like to disable the module but Drupal doesn’t permit. To solve problem, go to ‘field_config’ table and update the ‘type’ and ‘module’ text for your custom field. More info on the issue https://drupal.org/node/1284380

Fields pending deletion

This comes up after the custom field is removed but the chrom job run only once which is not sufficient. It needs to run several times.

Reference:

  • http://clikfocus.com/blog/how-set-custom-field-type-using-drupal-7-fields-api
  • http://drewpull.drupalgardens.com/blog/drupal-7-field-api-sample
  • http://drupaltutorial4u.blogspot.com/2011/04/how-to-create-custom-field-for-drupal-7.html
  • https://drupal.org/project/examples
  • http://drewpull.drupalgardens.com/blog/drupal-7-custom-field-formatter
  • http://drupal.stackexchange.com/questions/49958/hook-field-schema-does-not-work
  • http://nodesforbreakfast.com/article/2012/02/24/create-custom-form-api-elements-hookelementinfo-drupal-7
  • https://api.drupal.org/api/drupal/includes%21database%21schema.inc/group/schemaapi/7
  • http://www.slideshare.net/zugec/fields-in-core-how-to-create-a-custom-field

Activate Context To Rebuild Page More Than Once Per Page Load in Drupal

There was a situation where we needed to rebuild a page content and do it from Page scope(hook_preprocess_page). Most of the time the page content is altered via hook_page_alter(), however. This adds overhead of setting up a separate function, filtering request and other things all of which creates another dependency to the logic already handled from the hook_preprocess_page(). In this post, we cover how to accomplish the same thing from Page scope(hook_preprocess_page) to force rebuild content after making changes to it.

Here, we use Context module to manage blocks.

1. Activate Context

The context is what sets which blocks to display and in which region. Depending on the request, we activate a different context as following:

function MODULE-NAME_preprocess_page(&$vars){
      if(module_exists('context')){
            $context = context_load('shop');
            (isset($context->name)) ? context_set('context', $context->name, $context) : watchdog(WATCHDOG_NOTICE, 'Context with name - shop does not exist');
     }
}

Here, the context – shop is loaded and then set to be active.

The changes are made, however, the blocks and regions are already built and set in the $vars[page] variable ready to be rendered in the template file.

2. Rebuild the Blocks and Regions

Although we activated certain context, the blocks of this context will not show up unless we rebuild the blocks and regions for the content:

function MODULE-NAME_preprocess_page(&$vars){
...
//rebuild blocks and regions
//            block_page_build($vars['page']);
            context_page_build($vars['page']);

            //set the new changes in cache
            drupal_set_page_content($vars['page']);
...
}

Here, first we rebuild blocks managed by context in the line 5. In the line 4, we are including uncommitted line if the changes would have been made for blocks managed by Block module. Afterwards, the cache for the content is reset with our new changes.
Thats all, now the content is displayed with the new changes

Breadcrumbs in Drupal

In this post, we go over manipulating the structure while altering the presentation of breadcrumbs in Drupal Framework

When the task is to update breadcrumbs in Drupal, we try to separate the task into two sub-tasks:

  • Manipulating Breadcrumbs Structure
  • Altering Breadcrumbs Presentation

Manipulate Breadcrumbs Structure

We have found that the best way to manipulate breadcrumbs structure is in the page preprocessor function(in template.php file). Lets, say we need to insert additional item in the breadcrumbs list as the last element as well as add icon img in front of first item:

function THEMENAME_preprocess_page(&$vars, $hook) {
    $node = $vars['node'];
    if (isset($node)) {
        $breadcrumb = drupal_get_breadcrumb();
        $breadcrumb[0] = '<i class="icon-home home-icon"></i>'.$breadcrumb[0];
        $breadcrumb[] = $node->title;
        drupal_set_breadcrumb($breadcrumb);
    }
}

In the line five, the array of breadcrumbs is retrieved. In the line six, we add an icon in front of the first element of the breadcrumbs. In the line seven, we add additional item that is the title of current page at the end for breadcrumbs

A better way to manipulate Breadcrumb structure might be via hook_menu_breadcrumb_alter(&$active_trail, $item) hook as following:

function HOOK_menu_breadcrumb_alter(&$active_trail, $item) {
    
         $end = end($active_trail);
        if ($item['href'] == $end['href']) {
            //the last element is of the current page
            $last = array_pop($active_trail);//store the last temporarily
            $active_trail[] = array(
                'title' => 'Portfolio',
                'href' => base_path().'path/to/portfolio',
                'localized_options' => array(),
            );
            //put back the last element
            $active_trail[] = $last;
        }else{
            $active_trail[] = array(
                'title' => 'Portfolio',
                'href' => base_path().'path/to/portfolio',
                'localized_options' => array(),
            );
        }
}

Here, we first check to see if the laset element of the current breadcrumb trail is for the current page. If so, we store it before putting our own new breadcrumb element ‘Portfolio’. As you see, there is three elements – title, href and localized_options to create new breadcrumb element and by adding to the active trail array, we accomplish the task

Alter Breadcrumb Presentation

One way to alter presentation is int he hook_breadcrumb function(in tempalate.php file)as following:

function THEME-NAME_breadcrumb($variables) {
    $breadcrumb = $variables['breadcrumb'];

    if (!empty($breadcrumb)) {
        $breadcrumbs = '<ul class="breadcrumb">';

        $count = count($breadcrumb) - 1;
        foreach ($breadcrumb as $key => $value) {
            if ($count != $key) {
                $breadcrumbs .= '<li>' . $value . '</li>';
            }
            else{
                $breadcrumbs .= '<li class="active">' . $value . '</li>';
            }
        }
        $breadcrumbs .= '</ul>';

        return $breadcrumbs;
    }
}

We are overriding the bootstrap implementation and updating with our new theme requirements. In line 6, the appropriate class attribute value is added. In lines 14, the separator(‘active’ class) is inserted per our theme design.

Render Breadcrumbs

To render breadcrumbs:

      <?php if (!empty($breadcrumb)): print $breadcrumb; endif;?>

Issues

function drupal_set_breadcrumb() is not defined
This error appear when parameter of the function drupal_set_breadcrumb() is not in a proper format(needs to be array of links)

WYSIWYG with CKEditor in Drupal

It appears that CKEditor is the most advance and aesthetic WYSIWYG editor available for Drupal. As of February 2014, we run into issues installing the CKEditor as regular way WYSIWYG editors are installed in Drupal. In Part I, we summarize it. In the second part, we cover alternative integration of CKEditor as a stand alone editor

Install CKEditor Part of DRUPAL WYSIWYG

1. Enable WYSIWYG [wysiwyg] editors

To enable WYSIWYG editors in Drupal, install and enable wysiwyg module
With drush:

drush dl wysiwyg
drush en wysiwyg
2. Install Editor of your choice

Once module wysiwyg installed, then got to ‘admin/config/content/wysiwyg’ to see all the available wysiwyg editors with instructions how to install and enable any from the list.

We follow instruction and download the CKEditor library(in our case 4.3.2) from their site and extract it into the ‘site/all/libraries’ so that ‘ckeditor/ckeditor.js’ is as specified in the WYSIWYG configuration page.

After doing all this, the CKEDitor is not working and we have error:

The version of CKEditor could not be detected.

An alternative approach to install CKEditor as a separate module not part of WYSIWYG setup

Install CKEditor As Separate Module

Before continuing with this approach, make sure to disabale module – wysiwyg. The first step is as described above step 2 – Install Editor of Your Choice

A) Install the CKEditor Library

Download and past the CKEditor library into folder sites/all/libraries, so that ckeditor.js is in ‘sites/all/libraries/ckeditor/ckeditor.js’

B) Installing CKEditor Module

With Drush:

drush dl ckeditor
drush en ckeditor
C) Configure to use CKEditor for input fields

Go to admin/config/content/ckeditor/edit/Advanced or admin/config/content/ckeditor/edit/Full and ensure the checkbox ‘Text Formats’ for “Filtered HTML” or “Full HTML” is selected under ‘Basic Setup’ section.

For more custom configuration, go to ‘admin/config/content/ckeditor/’

D) Test Drive

Just open a node to ensure the editor is coming up for the input field with ‘full’ or ‘filter’ text formats

Other Stuff

Configure CKEditor Outside Drupal

If limited to what Drupal module – CKEditor configurations(admin/config/content/ckeditor) provides then you can configure CKEditor described in the CKEditor documentation native of CKEditor by updating the modules/ckeditor/ckeditor.config.js

For example:
To disable the advance auto filtering so that when CKEditor is enabled, it does not add and change extra html of the content:

CKEDITOR.editorConfig = function(config) {
    config.allowedContent = true;
...
}

This will disable the auto advance filtering. There other configurations to make like changing the color of the editor:

CKEDITOR.editorConfig = function(config) {
    config.uiColor = '#AADC6E';
...
}
Configure Allowed HTML Tags

To configure which html tags are allowed, consider to use wysiwyg_filter

sudo drush dl wysiwyg_filter
drush en wysiwyg_filter

Also uncheck ‘Limit allowed HTML tags’ in the Home » Administration » Configuration » Content authoring » Text formats

In addition, the CKEditor itself comes preconfigured to remove empty tags such span, em by default. Please see post Taking CKeditor Apart on how to avoid removing empty tags

Insert Photos in Content via IMCE Module

In order to insert photos in content via WYSIWYG, there is a Drupal Module called IMCE. To install and enable:

sudo drush dl imce
sudo drush en imce

This will install the necessary module to be able upload and insert photos via WYSIWYG editor in our case CKEditor

A) Make part of Image Dialog
The IMCE can be integrated in the CKEditor Image dialog. One of the benefits is that the functionality is add to current UI control(image button) so there is no need for additional buttons in the CKEditor Toolbar. Go to admin/config/content/ckeditor/edit/Full(or Advance) and select ‘IMCE’ for ‘File browser type (Image dialog window)’ in the section ‘FILE BROWSER SETTINGS’.

B)Make IMCE As Separate Control
The IMCE can be used as separate with its own button to upload files. To do that, check ‘Plugin for inserting files from IMCE without image dialog’ in the admin/config/content/ckeditor/edit/Full under ‘Plugins’ section. This will enable a separate IMCE button to be added to the CKEditor Toolbar.

Enhance Images to Resize, Align and Wrap text

With the newest version of CKEditor lib (4.4.0 and above) there is no more need for Drupal ckeditor_image module to have image enhancements such us resizing, aligning and wrapping text around. It comes out of box. You may want to combine the CKEditor Full source with plugin ‘Enhanced Image‘(image2) to have additional UI functionality for resizing by simply dragging a mouse.

Insert Photos in Content via Media & CKEditor_Media Module

Another module to insert images into content is Media. The benefit of using the Media module is that with ckeditor_media you are able to use the Media browser UI instead the CKEditor.
To install:

drush dl media
drush dl ckeditor_media
drush en media, ckeditor_media

Afterwards, configure the CKeditor toolbar to have the button for media browser by going to admin/config/content/ckeditor/edit/Full and drag/drop the media button.

Enabling CKEditor For Textarea

If you have custom form with textarea, then to enable the CKEditor, add the following:

          'some_textarea' => array(
            '#type' => 'text_format',
            '#format' => 'full_html',
            '#rows' => '5',
            '#prefix' => t('Content'),
            '#default_value' => variable_get('some_textarea', ''),
        ),

By adding ‘text_format’ and ‘full_html’ to the textarea element,it will trigger the CKEditor to load

Adding Theme Styles

http://docs.ckeditor.com/#!/guide/dev_styles

How to Externalized CKeditor settings?

Thomas asked the question how to externalized CKeditor settings(ckeditor/ckeditor.config.js)
Here is list of alternative potential solutions:

      1. It is possible to force ckeditor to read config file from theme path instead of module path. Its a setting under ckeditor / advanced – and then you can just copy the custom config file into the theme folder.
      2. [NEED TO BE TESTED] Use hook_js_alter to replace default ckeditor with your custom one. See post How To Upgrade JQuery UI In Drupal for the same approach just instead altering jQuery lib you would alter ckeditor.config.js location.
      3. [NEED TO BE TESTED] If overlays are not used for admin content then you can have custom Js floated with your custom settings set on the CKEDITOR variable. See post Taking CKEditor a Apart and look how CKEditor is started from console. This should give you idea on how to call CKEditor API on CKEDITOR global js variable with your custom settings
      Note:I don’t remember anymore, was it because overlays are in separate iframe or because CKEditor Drupal module removes CKEDITOR global var on load that makes it unavailable for calling CKEditor API directly. That being said, the second approach may not work

The first approach is more for UI development, the second is more for Drupal developers and third is more JS developers that like to have it done in js instead within Drupal framework

Troubleshooting

1. Many of the CKEditor Toolbar buttons doesn’t show

Make sure you have installed the FULL CKEditor library and not the other two. If a particular button don’t show up then the plugin or the code for that functionality is not present

2. The text doesn’t show up in CKEditor

This may be because the text color of theme is white and with CKEditor background color being white as well, the text appears missing. By changing “Editor CSS” in admin/config/content/ckeditor/edit/TEXT_FORMAT to “CKEditor Default” will use the CKEditor Theme with text font of color black

3. IMCE doesn’t work

The IMCE needs to be configured to work for all users. Go to admin/config/media/imce and create profile. Afterwards, assign this profile to user role

The Upload, Insert File, Resize,etc Buttons Doesn’t Show Up In IMCE Browser

Make sure the permissions for the public dir is set correctly

Useful links

  • CKeditor Source
  • https://drupal.org/project/ckeditor
  • http://docs.ckeditor.com/#!/guide/dev_configuration
  • http://docs.ckeditor.com/#!/api/CKEDITOR.config
  • http://docs.cksource.com/CKEditor_for_Drupal/Open_Source/Drupal_7/Tricks
  • http://drupal7tutorials.com/tutorial/3-getting-your-ckeditor-setup-drupal-7
  • http://docs.cksource.com/CKEditor_for_Drupal
  • https://drupal.org/node/1827434
  • http://ckeditor.com/forums/Support/How-to-build-and-implement-my-own-data-processor-and-disable-the-build-in-processor