Tuesday, June 26, 2018

Drupal module challenge

Drupal module challenge



During the holidays Ive stumbled upon this website: moduleoff.com. It offers regular module development quests for various prices. For the first one it was Nexus 7 tablet, so I applied :) Now Id like to explain my solution.


The problem was the following:

"Its a pretty common use case. You need a button on a node that says something like "Mark as Complete" or "Set as Today" which updates a date field. In this challenge, youll need to add a link to the view page of every article node that will update a date field to now using an AJAX request. The date field should be updated in the database as well as on the currently displayed node, without a page load."

I fired up a Drupal 7 site and created an info file:

name = Set It Now
description = "Provides call to action to set date field values to now."
package = Date/Time
core = 7.x

dependencies[] = date


I had only one dependency to Date module. So whats next? There will be a link after each date field on an entity. To make it work we have a nice hook called hook_field_attach_view_alter.

/**
* Implements hook_field_attach_view_alter().
*/
function set_it_now_field_attach_view_alter(&$output, $context) {
}


Now I realized I only want to show the link to users having a dedicated permission, so I created one:

// Permission to view the call to action link.
define(SET_IT_NOW_VIEW_PERMISSION, view set it now link);

/**
* Implements hook_permission().
*/
function set_it_now_permission() {
return array(
SET_IT_NOW_VIEW_PERMISSION => array(
title => t(View set-it-now link),
description => t(User can see and use link to set the a date field to ow),
),
);
}


Now I can stop adding my link when the user has no permission to do so inside set_it_now_field_attach_view_alter():

 // Dont show link if permission is not granted.
if (!user_access(SET_IT_NOW_VIEW_PERMISSION)) {
return;
}


Now comes the fun part. I have to check all rendered field, check if its a date field and add my extra link element. Its a bit tricky since the rendered item is a single string. Into set_it_now_field_attach_view_alter:

 $field_names = element_children($output);
// Check all fields on the entity for date fields.
foreach ($field_names as $field_name) {
if (in_array($output[$field_name][#field_type], array(datetime, date, datestamp))) {
$field_definition = field_info_field($field_name);
$is_to_date = !empty($field_definition[settings][todate]);

// Loop through each delta.
foreach ($output[$field_name][#items] as $delta => $item) {
static $entity_info;
if (!isset($entity_info)) {
list($entity_id, $entity_vid, $bundle_type) = entity_extract_ids($context[entity_type], $context[entity]);
}
// Alter the output by adding the call to action.
$output[$field_name][$delta][#markup] = theme(set_it_now_link, array(
entity_type => $context[entity_type],
entity_id => $entity_id,
field_name => $field_name,
delta => $delta,
view_mode => $context[view_mode],
is_to_date => $is_to_date,
original => $output[$field_name][$delta][#markup],
));
}
}
}


Now the problem is that the rendered output is a single string and I have to attach my call to action. I created a template - its convenient anyway to have something overridable and at the same time I can separate it from the attach logic. The theme has to know about the original value and some field/entity related properties, lets create the hook_theme:

/**
* Implements hook_theme().
*/
function set_it_now_theme($existing, $type, $theme, $path) {
return array(
set_it_now_link => array(
variables => array(
entity_type => NULL,
entity_id => NULL,
field_name => NULL,
delta => NULL,
view_mode => NULL,
is_to_date => NULL,
original => NULL,
),
),
);
}


The theme is:

/**
* Theme function of set_it_now_link.
*
* @param $variables
* Variables array.
* @return string
* @see set_it_now_theme()
*/
function theme_set_it_now_link($variables) {

}


The link should contain all the necessary parameters to change the right fields value:

 // Add standard Drupal ajax handler.
drupal_add_library(system, drupal.ajax);

// Link to change the first date.
$link = l(
t([set it now]),
"set_it_now/{$variables[entity_type]}/{$variables[entity_id]}/{$variables[field_name]}/{$variables[delta]}/{$variables[view_mode]}/" . SET_IT_NOW_DATE_FROM . "/nojs",
array(
query => drupal_get_destination(),
attributes => array(class => use-ajax),
)
);


I added the Drupal Ajax functionality so we can use it both with and without JS. When we render the output we have to take care of the ajax upadte. I found the best to wrap the output and tag it with a very specific class:

 // Mark the field wrapper so it can be replaced easily.
$class = set-it-now- . $variables[entity_type] . - . $variables[entity_id] . - . $variables[field_name] . - . $variables[delta] . - . $variables[view_mode];

return <span class=" . $class . "> . $variables[original] . $link . $link_to . </span>;


Now we can create the menu for the call:

/**
* Implements hook_menu().
*/
function set_it_now_menu() {
$items = array(
/**
* Setting action callback.
*
* Params:
* #1 entity type
* #2 entity id
* #3 field name
* #4 delta
* #5 view mode
* #6 from or to date value
* #7 nojs/ajax.
*/
set_it_now/%/%/%/%/%/%/% => array(
type => MENU_CALLBACK,
access arguments => array(SET_IT_NOW_VIEW_PERMISSION),
page callback => set_it_now_set_date,
page arguments => array(1, 2, 3, 4, 5, 6, 7),
delivery callback => ajax_deliver,
),
);

return $items;
}


One trick here is using the delivery callback attribute so Drupal knows how to handle. Lets create the callback:

/**
* Page callback for setting the Date field (set_it_now/%/%/%/%/%).
*
* @param $entity_type
* Type string of the entity that has the Date field.
* @param $entity_id
* ID integer of the entity.
* @param $field_name
* Name of the Date field.
* @param $delta
* Delta integer of field item.
* @param $view_mode
* Entity display view mode string.
* @param $date_field_value
* Flag showing first or second field value it is.
* @param $nojs
* Indicator string to show if the request is ajax.
* @return array|void
* Commands to replace the HTML if ajax. Reload the page otherwise.
* @see set_it_now_menu()
*/
function set_it_now_set_date($entity_type, $entity_id, $field_name, $delta, $view_mode = full, $date_field_value = SET_IT_NOW_DATE_FROM, $nojs = nojs) {

}


We need to gather some information about the entity and the field to provide the proper values:

 // Get the entity object.
$entities = entity_load($entity_type, array($entity_id));
$entity = $entities[$entity_id];

// Get field definition.
$field_definition = field_info_field($field_name);


Then we have to get the field (I cycle through all the language instances - Im not sure if thats really important but its convenient) and set the proper value - depending to the field type. When its all set we can save it. We can use facade functions like node_save() or check if the entity module installed and use entity_save():

 // Set value for all languages.
foreach ($entity->{$field_name} as $lang => $item) {
$value_name = $date_field_value == SET_IT_NOW_DATE_FROM ? value : value2;
switch ($field_definition[type]) {
case datetime:
$entity->{$field_name}[$lang][$delta][$value_name] = format_date(time(), custom, Y-m-d H:i:s);
break;
case date:
$entity->{$field_name}[$lang][$delta][$value_name] = format_date(time(), custom, Y-m-dTH:i:s);
break;
case datestamp:
$entity->{$field_name}[$lang][$delta][$value_name] = time();
break;
default:
continue;
}

// Save and update entity.
if ($entity_type == node) {
node_save($entity);
}
elseif (module_exists(entity)) {
entity_save($entity_type, $entity);
}
else {
drupal_set_message(t(Date value cannot be changed. It is attached to an unknown entity type. Please install the Entity module.), warning);
}
}


If all done we can first check the request type. If it was ajax we prepare a command array and let it go:

 // If request was ajax then respond with a replacement command.
if ($nojs == ajax) {
$field_view = field_view_field($entity_type, $entity, $field_name, $view_mode);

$commands = array();
$selector = span.set-it-now- . $entity_type . - . $entity_id . - . $field_name . - . $delta . - . $view_mode;
$commands[] = ajax_command_replace($selector, $field_view[$delta][#markup]);
return array(
#type => ajax,
#commands => $commands,
);
}


Or using the destination in the url we just redirect it to the origin:

 // Non JavaScript request will redirect the page.
return drupal_goto();


And thats all. I didnt paste all the code here, but you can check it in my sandbox repo.

Also - what a shame - there is a demo video about it:



That challenge was won by Jake Bell as you can see on the page. Ive checked his solution and to be honest thats just brilliant :) Congratulations!

---

Ill soon write a blogpost about other programming challenges. Let me know if you met any other similar site.

Peter

go to link download