Jan 07 2019
Jan 07
The home automation system is a combination of software and hardware devices which handles home routine such as lights, temperature/humidity (microclimate) and entertainment systems (TV, audio etc.) automatically based on some scenarios. This post is about building such system based on Domoticz server, Xiaomi and Broadlink devices. I'll try to cover some topics such as system set up and useful scenarios. Agenda:
  1. Hardware
    1. Gateway
    2. Signaling sensors
    3. Data provider sensors
    4. Controlling sensors
    5. Smart lamps
    6. Server and wifi router
  2. Software
    1. Domoticz
    2. Mini DLNA
  3. Adding devices to Domoticz
    1. Gateway
    2. ZigBee devices
    3. Yeelight Smart bulbs
    4. Broadlink RM 3 mini and RM Pro+ remote controls
    5. Naming convention, groups, and scenes
  4. Network
  5. Scenarios
    1. Network devices state
      1. Are you at home?
    2. Notifications
      1. Motion activity detected
      2. Doors/windows activity detected
      3. Fire detected (high temperature)
      4. ZigBee devices batteries level report
    3. Room occupation
      1. Motion trigger
      2. Windows and doors trigger
    4. Room presence detection
      1. TV
      2. Laptop
      3. Room locking
    5. Light
      1. Room occupation trigger
      2. Button trigger
    6. Microclimate
    7. Alarm clock
  6. Remote access to your Domoticz
  7. Monitoring
    1. Service uptime
    2. Containers state
  8. Conclusion

Hardware


Let's overview hardware which will be used in the system. I distinguish 5 types of devices/sensors for building home automation systems: gateways, signaling sensors, data providers, controlling sensors and "smart" light. Most of Xiaomi/Aqara devices are working over ZigBee protocol, but there are some which are working over wifi like lamps. In a nutshell, a gateway is a central place of a smart home which receives all the data from other sensors/devices which are connected to it. Signaling sensors tell about some event. Data sensors provide environment information. Controlling sensors send commands to home devices. Smart lamps can be controlled over wifi in order to switch light.
There are a lot more devices and sensors on the market for different purposes but in this post, I'll cover only that which are used for my basic scenarios.

Gateway

  • Gateway (Xiaomi, WiFi/ZigBee) - a central point of ZigBee network. Receives events from other sensors. A system can run scenarios based on provided information.

Signaling sensors

  • Motions sensor (Xiaomi or Aqara, ZigBee) - emits a signal when motion is detected.
  • Door/window state sensor (Xiaomi or Aqara, ZigBee) - emits a signal when door/window is being opened or closed.

Data provider sensors

  • Temperature and humidity sensor (Xiaomi or Aqara, ZigBee) - provides current temperature and humidity.
  • Lux sensor (Aqara, ZigBee) - provides current luminosity. Usually, this sensor combined with a motion sensor (Aqara).

Controlling sensors

  • Button (Xiaomi, ZigBee) - sends commands (single click, double click and long click).
  • Socket (Xiaomi or Aqara, ZigBee) - power control.
  • Remote control RM Pro + (Broadlink, WiFi) - sends commands to IR and RF controlled devices such as conditioner, TV etc.
  • Remote control RM Mini (Broadlink, WiFi) - sends commands only to IR controlled devices.

Dec 26 2018
Dec 26
It's not possible to use list values in a conditional operator. For example, if you try:
resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${var.es_zone_awareness_enabled ? list("subnet-1", "subnet-2") : list("subnet-1")}" ]
  }

  ...
}
it will fail with "conditional operator cannot be used with list values" message. This is because terraform cannot assert that list's element types are consistent.

The workaround is to join list values into a string to bypass type check and then split string back to list:

resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${split(",", var.es_zone_awareness_enabled ? join(",", list("subnet-1", "subnet-2")) : join(",", list("subnet-1", "")))}" ]
  }

  ...
}
You might've mentioned that there is an empty string as a second list value in second join function. This is needed to make result string contain a "," character for splitting by final split function.

Of course, it's just a synthetic example with list functions. In real life, those lists come from variables. For instance, I had a map variable which looked like

variable "private_subnet_ids" {
  type = "map"
  default = { "zone-1,zone-2" = "subnet-1,subnet-2" }
}
and I wanted to get a list which contains one or both subnet ids depending on bool variable. Possible steps were:
  1. Turn a map into a list.
  2. If we need both subnets:
    1. Get the first list value (string, "subnet-1,subnet-2").
  3. If we need only one subnet:
    1. Get the first list value (string, "subnet-1,subnet-2").
    2. Split by "," character (list, ["subnet-1", "subnet-2"]).
    3. Get needed list element (string, "subnet-1").
    4. Put it to a list along with an empty string (list, ["subnet-1", ""]).
    5. Join this list into a string by "," character (string, "subnet-1,").
  4. Split by "," character.
Result resource definition looked as:
resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${split(",", var.es_zone_awareness_enabled ? element(values(var.private_subnet_ids), 0) : join(",", list(element(split(",", element(values(var.private_subnet_ids), 0)), 0), "")))}" ]
  }

  ...
}
Jan 27 2018
Jan 27
Netflix Eureka is a REST service that is primarily used in the AWS cloud for locating services. It comes with a Java-based client. But if you need a PHP-based client - welcome under the cut.

PHP Eureka client supports all the Eureka operations: register, de-register, send heartbeat, take instance out, put instance back, update metadata etc. How to use:

1. Create Eureka app instance definition:
// We will use app name and instance id for making requests below.
$appName = 'new_app';
$instanceId = 'test_instance_id';

// Create app instance metadata.
$metadata = new Metadata();
$metadata->set('test_key', 'test_value');

// Create data center metadata.
$dataCenterMetadata = new Metadata();
$dataCenterMetadata->set('data_center_test_key', 'data_center_test_value');

// Create data center info (Amazon example).
$dataCenterInfo = new DataCenterInfo();
$dataCenterInfo
  ->setName('Amazon')
  ->setClass('com.netflix.appinfo.AmazonInfo')
  ->setMetadata($dataCenterMetadata);

// Create Eureka app instance.
$instance = new Instance();
$instance
  ->setInstanceId($instanceId)
  ->setHostName('test_host_name')
  ->setApp($appName)
  ->setIpAddr('127.0.0.1')
  ->setPort(80)
  ->setSecurePort(433)
  ->setHomePageUrl('http://localhost')
  ->setStatusPageUrl('http://localhost/status')
  ->setHealthCheckUrl('http://localhost/health-check')
  ->setSecureHealthCheckUrl('https://localhost/health-check')
  ->setVipAddress('test_vip_address')
  ->setSecureVipAddress('test_secure_vip_address')
  ->setMetadata($metadata)
  ->setDataCenterInfo($dataCenterInfo);

2. Create Eureka client (it is based on Guzzle HTTP client):
// Eureka client usage example.
// Create guzzle client.
$guzzle = new Client();

// Create eureka client.
$eurekaClient = new EurekaClient('localhost', 8080, $guzzle);

3. Communicate with Eureka server:
try {
  // Register new application instance.
  $response = $eurekaClient->register($appName, $instance);

  // Send application instance heartbeat.
  $response = $eurekaClient->heartBeat($appName, $instanceId);

  // De-register application instance.
  $response = $eurekaClient->deRegister($appName, $instanceId);
}
catch (Exception $e) {
  echo $e->getMessage() . PHP_EOL;
}

See repository for more information.
Jan 21 2018
Jan 21
Using monolg library and monolog-cascade extension you can't configure the "namespaced" loggers. What does it mean? Imagine you have tons of classes and you need to log information from them into a log file. There is nothing special in this. Just define loggers with the needed handler(s) and instantiate them directly in a place where you want them to use with a help of monolog-cascade. It means in your monolog-cascade config file you have to define needed loggers in advance and you have to reference needed loggers by their names. But what if you need an additional logger (with absolutely different handlers/processors) for some of the classes? Will you go through all the classes and change logger names where you instantiate them? I think it doesn't look like a good idea when a small requirement (for instance, change the log file name for records from a bunch of classes) leads to edits in an application code. It's something that must be configurable and that's why I decided to write a tiny library called monolog-cascade-namespaced.

Monolog Cascade Namespaced is actually an extension of a monolog-cascade library (please take a look at this library first if you're not familiar with it). The main idea is simple - pass a full class name instead of logger name to the static method which instantiates loggers. Just do this:
CascadeNamespaced::getLogger(get_called_class())->info('...');
instead of this:
Cascade::getLogger('my_logger_name')->info('...');
In other words, you don't have to think which logger must be instantiated in this place. You will be able to configure needed logger in your config file instead of hardcoding the name here.

How does it work? First, it looks for a logger specific to your class. If there is no such logger then it looks for a logger for a namespace the class is located in. If there is no even this logger then it tries to find "root" logger - default logger for your application. And finally, if there is no even root logger defined in your config file then simple logger with null handler will be returned as a fallback behavior. Loggers specific to a class have more priority than loggers for a namespace.

Let's dive deeper and consider an example. You have next classes:

  • MyNamespace\Class1
  • MyNamespace\Class2
  • MyNamespace\SubNamespace\Class3
  • MyNamespace\SubNamespace\Class4
  • AnotherNamespace\Class5

and you want to log messages from MyNamespace\SubNamespace namespace classes into the stdout, but messages from MyNamespace\Class1 and MyNamespace\Class2 classes into two different files. It can be done with next configuration:
handlers:
    file1:
        class: Monolog\Handler\StreamHandler
        stream: log_file_1.log

    file2:
        class: Monolog\Handler\StreamHandler
        stream: log_file_2.log

    console:
        class: Monolog\Handler\StreamHandler
        stream: php://stdout

loggers:
    # Will be used for `AnotherNamespace\Class5` class as a default logger
    # because there are no specific loggers defined for this class.
    root:
        handlers: [console]

    # Will be used for all the classes from `MyNamespace\SubNamespace` namespace.
    MyNamespace\SubNamespace:
        handlers: [console]

    # Will be used for `MyNamespace\Class1` class.
    MyNamespace\Class1:
        handlers: [file1]

    # Will be used for `MyNamespace\Class2` class.
    MyNamespace\Class2:
        handlers: [file2]
If you need to change the logger for some class just define new one specific to a needed class in your config file. If you don't want to use any specific loggers then just leave root logger.

You may ask me "So, how does it differ from actually monolog-cascade"? The answer is:

  1. You don't have to define needed loggers in advance.
  2. You can define a logger for a set of classes located in a namespace.
  3. You can define a logger for a specific class.
  4. You don't have to hardcode a logger name. Just pass the full class name and configure logger in the future if needed.
Additional information on how to install, configure and use monolog-cascade-namespaced can be found here
Nov 26 2017
Nov 26
Let's imagine you want to send log records from your Drupal 8 site to your email box, 3rd party service or to some other destination in order to know about warnings, errors etc. Most probably you don't want to send an email each time when some action happens as you don't want to decrease page performance. So, in this case, you should write a "buffered logger" which will keep all log entries in a buffer and send them only when it's overflown or on shutdown function. So let's write it.

Define a Drupal logger


Let's say we have a custom module called logger_example, so create a directory src/Logger and put there a file BufferLogger.php (you can choose any name you want, it's just an example) with next content:
<?php

namespace Drupal\logger_example\Logger;

use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;

/**
 * Class BufferLogger
 *
 * @package Drupal\logger_example\Logger
 */
class BufferLogger implements LoggerInterface {

  use LoggerTrait;

  /**
   * @var array
   */
  private $buffer;

  /**
   * @var int
   */
  private $bufferLimit;

  /**
   * BufferLogger constructor.
   */
  public function __construct($bufferLimit = 10) {
    $this->buffer = [];
    $this->bufferLimit = $bufferLimit;

    drupal_register_shutdown_function([$this, 'flush']);
  }

  /**
   * Logs with an arbitrary level.
   *
   * @param mixed $level
   * @param string $message
   * @param array $context
   *
   * @return void
   */
  public function log($level, $message, array $context = []) {
    $this->buffer[] = [
      'level' => $level,
      'message' => $message,
      'context' => $context,
    ];

    // Flush buffer when it's full.
    if (count($this->buffer) == $this->bufferLimit) {
      $this->flush();
    }
  }

  /**
   * Log messages into needed destination.
   */
  public function flush() {
    // It's not "full buffer" case.
    // If buffer is empty it means it's usual call of shutdown function
    // and there is nothing to log.
    if (empty($this->buffer)) {
      return;
    }

    // TODO: Log buffered log entries here.

    // Reset the buffer.
    $this->buffer = [];
  }

}

Let's find out how does it work. First of all, it has $buffer and $bufferLimit properties. First one is an array which will contain all buffered records and the second one is a setting which means "how many log records do you want to keep before sending?".

In a constructor, we register a public method called flush as a PHP shutdown function which will actually send buffered records to the needed destination. We need this shutdown function to handle the case when the buffer isn't full but it isn't empty as well and we want to send those buffered records anyway.

The BufferLogger::log() method saves records in an array and flushes them if needed. So we do not send any log entries each time when log() is called.

The last method is BufferLogger::flush(). It does two things: sends buffered records into needed destinations and resets the buffer array. All sending logic must be implemented in this method.

Tell the Drupal about your logger


Define a service inside your logger_example.services.yml file and tag it with logger name:
services:
  ...
  logger.logger_example:
    class: Drupal\logger_example\Logger\BufferLogger
    arguments: [100]
    tags:
      - { name: logger }
  ...
You can set up buffer size as big/small as you wish. For example, you want to send logs only if the buffer is 100 messages full (or anyway it will flush the buffer on a shutdown). Rebuild Drupal cache and after that, Drupal will log messages through all defined loggers including yours and all buffered records will be sent to the needed destination.

That's all, now you have a simple buffered logger. Of course, it can be improved. For example, you could implement a filter for channels or/and log levels you want to listen to. It's useful when you need to be notified only, let's say, about errors from "system" channel. It's all up to you.

Nov 05 2017
Nov 05
If you need to install the latest version of a composer you can use next bash snippet:
EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php -r "if (hash_file('SHA384', 'composer-setup.php') === '${EXPECTED_SIGNATURE}') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer
Using EXPECTED_SIGNATURE variable with the latest available signature value you don't have to hardcode a specific one for comparison on 3rd line.
Jul 29 2017
Jul 29
This is a simple API module for Drupal 7 and Drupal 8 which allows developers to react on changed fields in a node when it was updated. For example, you want to modify node object depends on its field values. Or you just want to know what fields were changed. Or finally, you need to check the difference between old and new field values and do some other thing depending on this difference.

Changed Fields API supports all the core's field types both for Drupal 7 and Drupal 8. But for Drupal 7 it supports even more. Please visit the project page to find out more information.

Changed Fields API is built on "Observer" pattern. The idea is pretty simple: attach observers to a node subject which will notify all of them about changes in node fields. If you are not familiar with this pattern yet then I suggest you read about it and consider this simple example. So let's find out how to use this API. An example below is based on changed_fields_basic_usage and changed_fields_extended_field_comparator demo modules that are the part of Changed Fields API module.

Observer


The first thing we need is an observer. Define a class that implements ObserverInterface interface. This interface provides two methods: ObserverInterface::getInfo() and ObserverInterface::update(SplSubject $nodeSubject). The first method should return an associative array keyed by content type names which in turn contain a list of field names you want to observe. A second method is a place where you can react on changed fields and do something with a node. It will be called only if some of the listed fields were changed.

<?php

/**
 * @file
 * Contains BasicUsageObserver.php.
 */

namespace Drupal\changed_fields_basic_usage;

use Drupal\changed_fields\ObserverInterface;
use SplSubject;

/**
 * Class BasicUsageObserver.
 */
class BasicUsageObserver implements ObserverInterface {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    return [
      'article' => [
        'title',
        'body',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function update(SplSubject $nodeSubject) {
    $node = $nodeSubject->getNode();
    $changedFields = $nodeSubject->getChangedFields();

    // Do something with $node depends on $changedFields.
  }

}

So here we defined that we want to listen to article content type and we want to check only title and body fields.

Node presave


In order to detect changed fields in a node we need to define hook_node_presave and there do several steps:
  1. Wrap a node object into an instance of NodeSubject class and set up the field comparator. Field comparator is an object which checks needed fields and returns differences between old and new field values. Default comparator is default_field_comparator but you can define your own by extending default one. 
  2. Then attach your observer BasicUsageObserver to instance of NodeSubject class.
  3. Finally, execute NodeSubject::notify() method and react on this event in BasicUsageObserver::update(SplSubject $nodeSubject) if some of the field values of registered node types have been changed.

<?php

/**
 * @file
 * Contains changed_fields_basic_usage.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_basic_usage\BasicUsageObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_basic_usage_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by DefaultFieldComparator.
  $nodeSubject = new NodeSubject($node, 'default_field_comparator');

  // Add your observer object to NodeSubject.
  $nodeSubject->attach(new BasicUsageObserver());

  // Check if node fields have been changed.
  $nodeSubject->notify();
}

Basically, that's all but you can say "what about field types that is not supported by Changed Fields API. How can I handle them?". Well, you're right, API supports a limited bunch of field types but it's easy to extend it.

By default, Changed Fields API comes with default_field_comparator class which supports all the core Drupal's field types. But if you have a field type from a contrib module you have to tell the API how to compare that fields. In order to do that you have to write a custom field comparator.

Custom field comparator


In Drupal 8 version of the module field comparators are plugins and they should be placed into module_name/src/Plugin/FieldComparator directory. So we need to define a class, extend it from DefaultFieldComparator and override method DefaultFieldComparator::getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition). All that you need is return field properties to compare depends on a field type. For example for text_with_summary fields API returns array('value', 'summary').

<?php

/**
 * @file
 * Contains ExtendedFieldComparator.php.
 */

namespace Drupal\changed_fields_extended_field_comparator\Plugin\FieldComparator;

use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\changed_fields\Plugin\FieldComparator\DefaultFieldComparator;

/**
 * @Plugin(
 *   id = "extended_field_comparator"
 * )
 */
class ExtendedFieldComparator extends DefaultFieldComparator {

  /**
   * {@inheritdoc}
   */
  public function getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition) {
    $properties = [];

    // Return comparable field properties for extra or custom field type.
    if ($fieldDefinition->getType() == 'some_field_type') {
      $properties = [
        'some_field_property_1',
        'some_field_property_2',
      ];
    }

    return $properties;
  }

}

Here we assume that we have a custom field type some_field_type with properties some_field_property_1 and some_field_property_2. In order to compare such fields, we need to tell node subject that it should use our newly defined field comparator:

<?php

/**
 * @file
 * Contains changed_fields_extended_field_comparator.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_extended_field_comparator\ExtendedFieldComparatorObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_extended_field_comparator_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by your ExtendedFieldComparator.
  $nodeSubject = new NodeSubject($node, 'extended_field_comparator');
  ...
}

Cool, now we know how to add support for custom or additional contrib fields. But there might be cases when you want, for example, compare text_with_summary fields only by value property (by default API compares it by format and summary properties as well). In this case you need to override DefaultFieldComparator::extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) method like this one:

public function extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) {
    if ($fieldDefinition->getType() == 'text_with_summary') {
      unset($properties[array_search('summary', $properties)]);
      unset($properties[array_search('format', $properties)]);
    }

    return $properties;
  }

When it's done changes in field's summary or format properties will not be taken into consideration by the API.

Additional info

Jul 15 2017
Jul 15

On 10-11 of June in Kyiv there was an annual all-Ukrainian event - Drupal Camp Kyiv 2017. This is a place where experienced back-end, front-end developers, DevOps and managers share their knowledge. Traditionally Drupal Camp took place in two days: a conference day which includes 5 streams of presentations and code sprint where passionate developers can work together for improving Drupal and developing community. A few interesting statistics about this year’s event: 403 attendees, 5 streams of lectures, 42 speaker, 10+ international speakers, 70+ code sprint participants and 100+ patches made during code sprint.

I'm working on the Smartling company where my main projects are connectors: Smartling (for Drupal 7), TMGMT Smartling and TMGMT Extension Suite (for Drupal 8). The main goal both of these connectors is to send content for translation into Smartling service, get it back when translations are ready for publishing and correctly apply them into Drupal entities (such as nodes, taxonomy terms etc.), menus, blocks, locale strings and configurations.

So I took a part in this event with a presentation “How to outsource the pain of Drupal translation to Smartling” and I was talking about localization process in Drupal using Smartling translation service. I’ve also reviewed Translation Management Tool (a base for our connector), TMGMT Smartling and TMGMT Extension Suite modules.

In the session I've considered next key points:

  • How to configure your Drupal 8 site and TMGMT module
  • How to configure TMGMT Smartling plugin
  • What is visual context and why it’s so important for translators
  • How do we automate all the translation workflow and track changes of the source content
  • How did we manage to avoid a Drupal 8 core bug with bulk actions
I hope my session was interesting for those who wanted to know how to get rid of the pain of Drupal translation process and who was interested in other alternatives for translation workflow aside from common copy-pasting process from Excel sheets.

Drupal Camp Kyiv is the biggest Drupal event in Ukraine. It’s a great opportunity for developers to learn something new and grow up as a specialist. I’m looking forward to taking part in next events.


Session details


Jun 18 2017
Jun 18
Recently I set out to make a simple instrument for running simpletest tests without having LAMP stack installed on your local environment. I needed this for two reasons:
  1. for running tests locally
  2. for running tests on CI server
I've decided to use Docker and create monolith container with Drupal and all the LAMP stuff inside and here what I've got: docker-tester.


How to use


Before running container you have to setup next ennvironment variables:
  1. KEEP_RUNNING - specify  yes  if you want to keep container running when tests will be executed. Use for debugging purposes only. Default value is  no .
  2. DRUPAL_VERSION - specific version of Drupal. Supported Drupal 7 and Drupal 8. Example:  8.3.2 .
  3. MODULES_DOWNLOAD - a list of modules to download (by Drush) separated by comma. Example:  module_name-module_version,[...] .
  4. MODULES_ENABLE a list of modules to enable (by Drush) separated by comma. Example:  module_name,[...] .
  5. SIMPLETEST_GROUPS - a list of simpletest groups to run separated by comma. Example:  Group 1,[...] .
  6. SIMPLETEST_CONCURRENCY - amount of test runners to test code in parallel. Default value is  1 .
Then you need to build an image for container:
docker build -t drupal-tester .
Next you have two options: either run container with docker-compose tool or run container manualy with docker command.
For local usage I prefere to use docker-compose because it's easier than write all the CLI docker command manualy. Just specify what module you want to test inside of docker-compose.yml file and run:
docker-compose up && docker-compose down
It will run the container, install Drupal inside of it and run tests. That's all.

For running tests on CI server I use docker command and specify all the needed environment variables manualy:

docker run -v $(pwd)/test_results:/var/www/html/test_results -v $(pwd)/custom_scripts:/var/www/html/custom_scripts -e KEEP_RUNNING=no -e DRUPAL_VERSION=8.3.2 -e MODULES_DOWNLOAD=module-version -e MODULES_ENABLE=module -e SIMPLETEST_GROUPS=module_test_group -e SIMPLETEST_CONCURRENCY=1 drupal-tester
It allows you to override environment variables and volumes that you want to mount inside of the container. So you can setup different jobs on your CI server to test different modules on different Drupal versions with the help of this one container.

When docker finished the process all test results by default will be placed into test_results directory but you can easily override this by mounting some other directory inside of a container.

Setup and customization


Sometimes you need to do something before running tests. For example override some module specific settings or setup some Drupal variables etc. You can get it done with custom *.sh scripts. Just write sh file with all needed actions/commands and put it inside custom_scripts folder. All the files inside of this directory will be executed before running tests.
Mar 26 2017
Mar 26
Last time we've created a style plugin. Now we will learn how to cache panel panes with custom cache plugins.

In this tutorial we will create a panel cache plugin which will cache panel pane for a given time (5, 10 or 15 seconds). For now we have a module with next file structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
All that we need to do is put file example_panels_module.cache_plugin.inc into plugins/cache directory with next content:
<?php

/**
 * @file
 * Example panels cache plugin.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel cache'),
  // Plugin description.
  'description' => t('Example panel cache.'),
  // Cache get callback.
  'cache get' => 'example_panels_module_cache_get_cache',
  // Cache set callback.
  'cache set' => 'example_panels_module_cache_set_cache',
  // Cache clear callback.
  'cache clear' => 'example_panels_module_cache_clear_cache',
  // Settings form.
  'settings form' => 'example_panels_module_cache_settings_form',
  // Settings form submit.
  'settings form submit' => 'example_panels_module_cache_settings_form_submit',
  // Default values.
  'defaults' => [
    'lifetime' => 5,
  ],
];

/**
 * Get cached content.
 */
function example_panels_module_cache_get_cache($conf, $display, $args, $contexts, $pane = NULL) {
  $cache = cache_get('example_panels_module_cache:' . $pane->pid, 'cache_panels');

  // No cached data available.
  if (!$cache) {
    return FALSE;
  }

  // Cache is expired.
  if ((REQUEST_TIME - $cache->created) > $conf['lifetime']) {
    return FALSE;
  }

  return $cache->data;
}

/**
 * Set cached content.
 */
function example_panels_module_cache_set_cache($conf, $content, $display, $args, $contexts, $pane = NULL) {
 cache_set('example_panels_module_cache:' . $pane->pid, $content, 'cache_panels');
}

/**
 * Clear cached content.
 */
function example_panels_module_cache_clear_cache($display) {
  cache_clear_all('example_panels_module_cache:', 'cache_panels', TRUE);
}

function example_panels_module_cache_settings_form($conf, $display, $pid) {
  // Cache lifetime.
  $form['lifetime'] = [
    '#title' => t('Lifetime'),
    '#type' => 'select',
    '#options' => drupal_map_assoc([5, 10, 15], 'format_interval'),
    '#default_value' => $conf['lifetime'],
  ];

  return $form;
}
So now module structure looks like:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__|__ cache
  |__|__|__ example_panels_module.cache_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
Clear cache (on enable module if it isn't enable yet) and navigate to panel pages settings (for this tutorial I've created a test panel page for displaying a single node). Choose a panel pane to cache and set up a cache type as Example panel cache (for this tutorial I've created a panel pane that shows a REQUEST_TIME value and put it into node/view panel page):

Set up cache for a given panel pane

Then chose cache lifetime:

Cache lifetime

Save settings and create some test node. For example if you set up cache life time as 5 seconds then value will be static for 5 seconds and only after this time will be refreshed.

Key notes:


Source code of all examples are available here.

Feb 25 2017
Feb 25
Panels style plugins are made for wrapping panel panes and panel regions into extra markup. 99% of your needs are covered by a "Panels Extra Styles" module. Please look at that module if you need extra styles for panels. But if you need some specific style you can easily implement it.

In this tutorial we will create plugin style for region or pane. It will allow site builders to wrap region or pane into a custom markup entered in settings form.

1. Create a directory example_panels_module and put there a file example_panels_module.info:

name = Example panels module
description = Provides example implementation of panels plugins
core = 7.x

; Since we're using panels for creating
; plugins we have to define this
; dependency.
dependencies[] = panels
2. Tell ctools where it has to search plugins (file example_panels_module.module):
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools has to search plugins. Please note that
 * we should return path to plugins directory only
 * if $module equals 'panels'.
 */
function example_panels_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'panels' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
3. Create directory plugins/styles and put there a file example_panels_module.style_plugin.inc:
<?php
/**
 * @file
 * 'Example panel style' style.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel style'),
  // Plugin description.
  'description' => t('Raw HTML wrapper.'),
  // Render region callback.
  'render region' => 'example_panels_module_raw_wrapper_render_region',
  // Render pane callback
  'render pane' => 'example_panels_module_raw_wrapper_render_pane',
  // Region settings form.
  'settings form' => 'example_panels_module_raw_wrapper_region_settings_form',
  // Pane settings form.
  'pane settings form' => 'example_panels_module_raw_wrapper_pane_settings_form',
];

/**
 * Region settings form callback.
 */
function example_panels_module_raw_wrapper_region_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for region style.
  $form['wrapper_region_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_region_prefix']) ? $settings['wrapper_region_prefix'] : '',
  ];

  $form['wrapper_region_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_region_suffix']) ? $settings['wrapper_region_suffix'] : '',
  ];

  return $form;
}

/**
 * Region render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_region($vars) {
  $output = '';

  // Variable $vars['panes'] contains an array of all
  // panel panes in current region. Collect them into
  // variable.
  foreach ($vars['panes'] as $pane) {
    $output .= $pane;
  }

  // Variable $vars['settings'] contains settings
  // entered in settings form. Wrap region content
  // into custom markup.
  return $vars['settings']['wrapper_region_prefix'] . $output . $vars['settings']['wrapper_region_suffix'];
}

/**
 * Pane settings form callback.
 */
function example_panels_module_raw_wrapper_pane_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for pane style.
  $form['wrapper_pane_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_pane_prefix']) ? $settings['wrapper_pane_prefix'] : '',
  ];

  $form['wrapper_pane_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_pane_suffix']) ? $settings['wrapper_pane_suffix'] : '',
  ];

  return $form;
}

/**
 * Pane render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_pane($vars) {
  // Variable $vars['settings'] contains settings
  // entered in settings form. Variable
  // $vars['content']->content is a renderable array
  // of a current pane. Wrap pane content
  // into custom markup.
  return $vars['settings']['wrapper_pane_prefix'] . render($vars['content']->content) . $vars['settings']['wrapper_pane_suffix'];
}
For now we have a module with next structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
When it's enabled you will be able to setup style for a region or a pane. We've created style for both of them but you can define style only for region or only for pane.

Let's test our style. I want to edit node/%node page: wrap content region into div tag with a class "content-region-wrapper". And wrap node_beind_viewed pane into span tag with id "pane-node-view-wrapper". That's how can I do that:

Change style for a region

Select newly created style

Set up prefix and suffix for a region

The same way I've set up prefix and suffix for a panel pane and that's what I've got:

Result markup

Key notes:

Feb 14 2017
Feb 14
Relationships plugins are "bridge" between existing context (that is already set up in a panel) and a context which you want to get from existing one. Let's say your panel contains "Node" context and you want to get a node author (user from node:uid property). To do that you can just set up "Node author" relationship in a panel (under a "contexts" tab) and that's all. That's why relationships plugins are so important - they provide easy way for getting context from existing contexts. Please have a look at this post before continue reading - there is described how to create module integrated with ctools API which allows us to define own plugins.

As you remember we've created a custom server_info context plugin that provides information from $_SERVER superglobal php variable. All that you can grab from that context are strings. Ctools comes with a simple context plugin string. It provides three ways of string representation: html_safe, raw and uppercase_words_html_safe. In this tutorial we will create a relationship plugin that will convert a string from server_info to a string context.

Let's add new plugin to our existing example_module which for now looks like:

example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |     |__ example_module.example_argument_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:
<?php


/**
 * @file
 *
 * Plugin to provide a string relationship from server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('String from "Server info" context'),
  // Plugin description.
  'description' => t('Adds a string context from existing server_info context.'),
  // Keyword.
  'keyword' => 'string_from_server_info',
  // We want to create string context from server_info context.
  // It means that server_info has to be already set up into
  // a panel page. So server_info context is required for
  // this relationship.
  'required context' => new ctools_context_required(t('Server info'), 'server_info'),
  // Context builder function.
  'context' => 'example_relationship_plugin',
  // Settings form. We will provide a property from
  // server_info context we want to present as string context.
  'edit form' => 'example_relationship_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'server_info_property' => 'HTTP_HOST',
  ],
];

/**
 * Return a new context based on an existing context.
 */
function example_relationship_plugin($context = NULL, $conf) {
  $string_context = NULL;

  // If empty it wants a generic, unfilled context, which is just NULL.
  if (empty($context->data)) {
    $string_context = ctools_context_create_empty('string', NULL);
  }
  else {
    if (!empty($conf['server_info_property'])) {
      // Create the new string context from server_info parent context.
      $string_context = ctools_context_create('string', $context->data[$conf['server_info_property']]);
    }
  }

  return $string_context;
}

/**
 * Settings form for the relationship.
 */
function example_relationship_plugin_settings_form($form, $form_state) {
  $conf = $form_state['conf'];
  $keys = array_keys($_SERVER);

  $form['server_info_property'] = [
    '#required' => TRUE,
    '#type' => 'select',
    '#title' => t('"Server info" property'),
    '#options' => array_combine($keys, $keys),
    '#default_value' => $conf['server_info_property'],
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_relationship_plugin_settings_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
Module structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |  |  |__ example_module.example_argument_plugin.inc
  |  |__ relationships
  |     |__ example_module.example_relationship_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. After that you will see a new relationship on panel settings page under contexts tab (please note that you need to set up server_info context first before be able to set up a relationship based on server_info):

New relationship is available for this panel

3. Set up newly created relationship:

Choose a value from server_info context you want to represent as a string context

4. At this point you will see additional context on a panel - "String from 'Server info' context".

A string context available from server_info context

5. It means you can use, for example, %string_from_server_info:uppercase_words_html_safe token as a substitution for a page title. Let's try to set up title as "%server_info:HTTP_HOST : %string_from_server_info:uppercase_words_html_safe" and see what will happen:
Set up page title from contexts values

My host is "drpual.loc" so page title looks like "drupal.loc : Drupal.loc" where first part (before ":") is a value grabbed from server_info context and second part is a value from string_from_server_info context in "uppercase_words_html_safe" format.
Page title consists of context and relationship values

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:


Feb 12 2017
Feb 12
This time we will consider an argument plugin. Arguments are pretty similar to contexts. Actually arguments are context objects loaded from url. By default ctools provides a full set of needed arguments such as "Node: ID", "User: ID", "User: name" etc. But what if you've created a custom context? You might need to create a custom argument for your context (if you want to use your context as an argument of course). I advise you to read previous articles from "Ctools custom plugin" series such as "Ctools: custom access plugin" and "Ctools: custom context plugin". It's also required to read "Ctools: custom content type plugin" before reading this post because there I've described how to create a module integrated with ctools API which can contain ctools plugins.

For this tutorial we need a custom context (because there is no sense to copy paste default ctools argument plugins for creating default ctools context plugins - let's be creative) so I've created one: node_status. It's pretty simple and similar to default 'node' context by it provides extra node properties: 'status' and 'promote' (default node context doesn't provide them). It behaves similar to 'node' context. I mean it has a 'nid' option in settings form. Once you've entered a node id you will be able to use %argument_name:status and %argument_name:promote values. Context definition looks like:
<?php

/**
 * @file
 *
 * Plugin to provide a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status'),
  // Plugin description.
  'description' => t('Node status.'),
  // A function that will return context.
  'context' => 'example_context_node_status_plugin',
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'node_status',
  // Context machine name.
  'context name' => 'node_status',
  // Settings form callback.
  'edit form' => 'example_context_node_status_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'nid' => 1,
  ],
  // Array of available context values.
  'convert list' => [
    'status' => t('Node status'),
    'promote' => t('Node promote'),
  ],
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_node_status_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_node_status_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('node_status');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin.node_status';

  if (empty($empty)) {
    // Define context data.
    // Variable $data can be an array or an object.
    // It depends on how this context is created.
    // If it's created by putting context directly to
    // a panel page then $data - array containing
    // settings from form.
    if (is_array($data) && !empty($data['nid'])) {
      $node = node_load($data['nid']);
    }

    // If context is created by
    // an argument then $data - object.
    if (is_object($data)) {
      $node = $data;
    }

    if (!empty($node)) {
      $context->data = $node;
      $context->title = $node->title;

      // Specify argument value - node id.
      $context->argument = $node->nid;
    }
  }

  return $context;
}

/**
 * Returns property value by property type.
 */
function example_context_node_status_plugin_convert($context, $type) {
  $result = '';

  // Return node property (status or promote).
  if (!empty($context->data)) {
    $result = $context->data->{$type};
  }

  return $result;
}

/**
 * Settings form for cookies context.
 */
function example_context_node_status_plugin_settings_form($form, &$form_state) {
  // Node id option.
  $form['nid'] = [
    '#type' => 'textfield',
    '#title' => t('Node nid'),
    '#default_value' => $form_state['conf']['nid'],
    '#element_validate' => [
      'element_validate_integer_positive',
    ],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_node_status_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['nid'] = $form_state['values']['nid'];
}
I won't describe what goes here because we've already discussed custom context theme last time. Just put this code into example_module/plugins/contexts/example_module.example_context_plugin.node_status.inc file. Make sure that your module file structure now looks like this one:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |     |__ example_module.example_context_plugin.node_status.inc
  |__ example_module.info
  |__ example_module.module
Ok, finally we've got a context which we want to use as argument. For this tutorial I've created a custom page with a "node-view-page/%nid" url.

Page with required argument

Let's move on. Now we will create an argument plugin.

1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:

<?php

/**
 * @file
 *
 * Plugin to provide an argument handler for a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status argument'),
  // Plugin description.
  'description' => t('Creates a node status context from a node ID argument.'),
  // Keyword.
  'keyword' => 'node_status_argument',
  // Context builder function.
  'context' => 'example_argument_plugin',
];

/**
 * Discover if this argument gives us the node we crave.
 */
function example_argument_plugin($arg = NULL, $conf = NULL, $empty = FALSE) {
  $context = FALSE;

  // If empyt it wants a generic, unfilled context.
  if ($empty) {
    $context = ctools_context_create_empty('example_module.example_context_plugin.node_status');
  }
  else {
    // We can accept either a node object or a pure nid.
    if (is_object($arg)) {
      $context = ctools_context_create('example_module.example_context_plugin.node_status', $arg);
    }
    elseif (is_numeric($arg)) {
      $node = node_load($arg);

      if (!empty($node)) {
        $context = ctools_context_create('example_module.example_context_plugin.node_status', $node);
      }
    }
  }

  return $context;
}
2. Clear cache and navigate to panel page settings: arguments. Assign "Node status argument" to %nid.

Assigned argument

3. Navigate to panel page settings: contexts. You will see that argument provides properties from defined context.
Context loaded from an argument

4. Now you can use context properties for example for page title:
Context properties are used in a page title

Now if you open node-view-page/1, for example, you will see:
Custom argument plugin in action

If you unpublish a node then title will be "Status: 0 Promoted: 1". That's all.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 11 2017
Feb 11
In previous post we created an access ctools plugin which can be used as a selection or visibility rule in panels. It's time to learn how to create another important custom plugin - a context. It provides additional information for a panel page. For example if you've put a node context to the page you will be able to use node properties as substitutions for a page title. Moreover you will be able to put node fields as panes to a page. By default ctools module provides useful contexts (node, user, taxonomy_term, entity etc) but you can define your own. Please, read first post of "Ctools custom plugins" series before continue reading this. There we've created a module integrated with ctools.

In this tutorial we will create a context plugin which will provide information about a server from superglobal php variable $_SERVER. As you remember for now we have an example_module with next file structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
To create a context plugin we need to create contexts directory and put there a file with plugin definition. Let's start:

1. Create example_module/plugins/contexts/example_module.example_context_plugin.inc file:

<?php

/**
 * @file
 *
 * Plugin to provide a server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Server info'),
  // Plugin description.
  'description' => t('Available information about a server.'),
  // A function that will return context.
  'context' => 'example_context_plugin',
  // Settings form callback.
  'edit form' => 'example_context_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'decode_request_uri' => FALSE,
  ],
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'server_info',
  // Context machine name.
  'context name' => 'server_info',
  // A function returns a list of
  // available values from a context.
  // By the way it can be just an array.
  'convert list' => 'example_context_plugin_get_convert_list',
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('server_info');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin';

  if (empty($empty)) {
    // Define context data.
    $context->data = $_SERVER;

    // Decode property if selected.
    // Just for demonstration how to use
    // plugin settings.
    if (!empty($conf) && !empty($data['decode_request_uri'])) {
      $context->data['REQUEST_URI'] = urldecode($context->data['REQUEST_URI']);
    }
  }

  return $context;
}

/**
 * Returns available properties list.
 */
function example_context_plugin_get_convert_list() {
  $list = [];

  // Get all $_SERVER properties and return them.
  foreach ($_SERVER as $property_name => $property_value) {
    $list[$property_name] = t('$_SERVER["!name"]', [
      '!name' => $property_name,
    ]);
  }

  return $list;
}

/**
 * Returns property value by property type.
 */
function example_context_plugin_convert($context, $type) {
  // Return $_SERVER property value.
  return $context->data[$type];
}

/**
 * Settings form for server_info context.
 */
function example_context_plugin_settings_form($form, &$form_state) {
  // Demo setting.
  $form['decode_request_uri'] = [
    '#type' => 'checkbox',
    '#title' => t('Decode $_SERVER["REQUEST_URI"] value'),
    '#default_value' => $form_state['conf']['decode_request_uri'],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['decode_request_uri'] = $form_state['values']['decode_request_uri'];
}
Updated module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache and set up Server info context for a panel page:

Newly created context

Plugin settings form

Available context properties

Let's test context and set up page title as %server_info:REQUEST_URI. By the way it's a node/%nid page on screeshot so to test it we should open a node view page (but you can set up this context to any panel page you want):
Page title will contain value from $_SERVER['REQUESTED_URI'] variable

Request uri as a page title

The last thing we have to test is an option "Decode $_SERVER["REQUEST_URI"] value". When it's checked uri will be decoded. Check this checkbox and open node view page with a query parameter such as "?test=parameter to decode" and update page. Browser will encode spaces in url and it will look like "?test=string%20to%20decode". But page title will contain original (decoded) value.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 05 2017
Feb 05
Last time we've learned how to create custom ctools content type plugin. In that post we've already created a module example_module where we defined the plugin. This time we will learn how to create custom ctools access plugin. This type of ctools plugins can be used as a selection rule for a panel variant or as a visibility rule for a panel pane. Please, read previous post before continue reading this. There is described how to create a module and integrate it with ctools.

Let's say we have a node/%node panel page where are placed "Powered by Drupal" and "Node being viewed" panes.

Node view panel page with "Powered by Drupal" and "Node being viewed" panes

And we want first panel pane be shown only if node title equals some specific string. For example "Test article". Well, for this case we have to create an access plugin and apply it to the panel pane. Let's start.
For now if you remember we have an example_module with next structure:
example_module
  |__ plugins
  |  |__ content_types
  |     |__ example_module.example_content_type_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/access/example_module.example_access_plugin.inc:
<?php

/**
 * @file
 * File with access plugin definition.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Example access rule'),
  // Plugin description.
  'description' => t('Example access rule'),
  // Access rule callback. This function has to return
  // TRUE of FALSE.
  'callback' => 'example_access_plugin',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to apply
  // this access rule to a pane or page variant. In this example
  // we need a 'Node' context.
  'required context' => new ctools_context_required(t('Node'), 'node'),
  // Settings form constructor callback. If you need custom submit/validate
  // callback you can define it by 'settings form submit' and
  // 'settings form validate' properties.
  'settings form' => 'example_access_plugin_settings_form',
  // Default values for edit form inputs.
  'default' => [
    'example_option' => 'example_value',
  ],
  // Summary callback. Returns plugin summary text which is visible
  // in drop-down setting menu of a pane.
  'summary' => 'example_access_plugin_summary',
];

/**
 * Settings form a pane.
 */
function example_access_plugin_settings_form(array $form, array $form_state, $conf) {
  // Please note that all custom form elements
  // should be placed in 'settings' array.
  $form['settings']['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $conf['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Access rule callback.
 */
function example_access_plugin($conf, $context) {
  $access = FALSE;

  // Variable $context contains a context grabbed by a panel page.
  // In this case it contains a node object in a 'data' property.
  if (!empty($context->data)) {
    $node_from_context = $context->data;

    // Access will be granted only if node title equals a string
    // set up in settings form.
    $access = $node_from_context->title == $conf['example_option'];
  }

  return $access;
}

/**
 * Summary callback.
 */
function example_access_plugin_summary($conf, $context) {
  return t('Example access plugin');
}
Now our module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. Apply newly created access rule for the 'Node being viewed' pane in node view panel page:

Set up visibility rule for a pane

Select 'Example access rule'

Set up string to match with a node title

It's time to test our access plugin. Create a node with title "Test" and look at it's page. There will be no "Powered by Drupal" widget because node title doesn't equal "Test article":

Node view page without "Powered by Drupal" widget

And now edit node and change title to "Test article". See what will happen:
Node view page with "Powered by Drupal" widget

Access plugin works correctly.

P.S.
As you could notice there is a little difference between content_type and access plugin definition:

  • 'edit form' VS 'settings form' properties.
  • 'defaults' VS 'default' properties.
  • you have to define custom inputs for access plugin settings form inside 'settings' array. At the same time there is no so strict rule for defining settings inputs for content type plugin.
  • in a form callback for content type plugins configuration is available in $form_state variable, but for access plugins it isn't there. It available in an extra variable $conf.


The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 04 2017
Feb 04
Ctools content types are an alternative to standard Drupal blocks. They are more comfortable and powerfull than blocks. Ctools content type plugins also known as panel panes. In this post you will learn how to create a configurable ctools content pane plugin.

For example we will create a custom content type plugin that will render a user name and user email. Let's start.

1. Create a module which will contain all defined plugins. File example_module/example_module.info:

name = Example module
description = Provides ctools content type plugin
core = 7.x

; Since we're using ctools api for creating
; ctools plugins we have to define this
; dependency.
dependencies[] = ctools
File example_module/example_module.module:
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools have to search plugins. Usually a place
 * where developers store all defined plugins is
 * "module_name/plugins" directory. Variable
 * $plugin contains a name of a plugin type. It can be
 * content_types, context, argument, cache, access. So all
 * of listed plugin types should be located in corresponding
 * subdirectories (plugins/content_types if you create a content
 * type plugin).
 */
function example_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
2. Create a file which will contain plugin definition. File example_module/plugins/content_types/example_module.example_content_type_plugin.inc:
<?php

/**
 * @file
 * File with plugin definition.
 */

$plugin = [
  // Pane title.
  'title' => t('Example pane'),
  // Pane description. 
  'description' => t('Example pane'),
  // Tell ctools that it's not a child of another pane.
  'single' => TRUE,
  // You can categorize all defined panes by
  // defining a category name for a pane.
  'category' => [t('Example panes')],
  // A machine name of your pane.
  'content_types' => ['example_content_type_plugin'],
  // Function that will render markup of this pane.
  'render callback' => 'example_content_type_plugin_render',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to add
  // this pane to the panel page.
  'required context' => new ctools_context_required(t('User'), 'user'),
  // Edit form constructor callback.
  'edit form' => 'example_content_type_plugin_edit_form',
  // Default values for edit form inputs.
  'defaults' => [
    'example_option' => 'example_value',
  ],
];
A file name is just a combination of a module name and plugin name joined by a dot. But actually you can name it as you want. Please note that you can omit 'required context' property if you want to be able to add this pane to all existing panel pages. If you want to add your pane only to pages that have a context just use required context like described above. If you want to add your pane to all pages (but in some cases you need a context) just define it as optional:
'required context' => new ctools_context_optional(t('User'), 'user'),
Let's define plugin configuration form:
/**
 * Configuration form for a pane.
 */
function example_content_type_plugin_edit_form($form, $form_state) {
  $form['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $form_state['conf']['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_content_type_plugin_edit_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
The last thing we shoud define is render callback:
/**
 * Render callback.
 */
function example_content_type_plugin_render($subtype, $conf, $args, $context) {
  $block = new stdClass();

  // Variable $context contains a context grabbed by a panel page.
  // In this case we will put this pane on user/%uid page which
  // has a user context.
  if (!empty($context->data)) {
    $user_from_context = $context->data;

    // Just output user's name and email.
    $block->content = 'User name: ' . $user_from_context->name . '</br>User mail: ' . $user_from_context->mail . '</br>Example option: ' . $conf['example_option'];
  }

  return $block;
}
3. Clear cache. Edit user view panel page and put this pane into some region:

Example ctools content type pane

Enter settings for a pane:

Configuration form

Open user/1 page and here's a result:

Rendered example ctools content type pane

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Jan 29 2017
Jan 29
Drush stands for "Drupal shell" which means a powerful tool for managing Drupal installation from command line interface. Drush provides a lot of useful commands for dealing with a cache, modules, cron, database etc. But some of contrib modules also provide some extra drush commands for specific functionality (like features module comes with commands for managing features). Here's a bunch of a useful drush commands which I use every day.


Cache

# Clear all caches.
drush cc all

# Clear menu cache.
drush cc menu

# Select cache bin for clearing.
drush cc

Modules

# Download module by name. You can grab module
# name from module's page on drupal.org.
drush dl module_name

# Download module by name.
# into sites/default/modules/custom
# directory.
drush dl module_name --destination=sites/default/modules/custom

# Enable module by name.
drush en module_name

# Disable module by name.
drush dis module_name

# Uninstall module by name.
# This commands will call hook_name_uninstall().
drush pmu module_name

# Get list of available modules
# in your Drupal installation.
drush pml

# Check if module module_name
# is available in your Drupal
# installation.
drush pml | grep module_name

User

# Get an one-time login link
# for user by his/her uid.
drush uli uid

# Get an one-time login link
# for user by his/her email.
drush uli [email protected]

# Block user by his/her username.
drush ublk test_user

# Unblock user by his/her username.
drush uublk test_user

# Change password for user by his/her name.
drush upwd --password="password" user_name

Pending database updates

# Run all available database updates
# implemented in hook_update_N() in
# *.install files. 
drush updb

Cron

# Run cron.php. It will call all hook_cron()
# defined in modules and process cron queues.
drush cron

Variables

# Get variable value by name.
drush vget variable_name

# Set variable value by name.
drush vset variable_name "Test value"

# Delete variable by name.
drush vdel variable_name

Registry

# Install registry_rebuild drush extension.
drush dl registry_rebuild

# Rebuild Drupal registry. Usefull when some of
# modules was moved into another folder.
drush rr

Features

# Get list of available features.
drush fl

# Get list of overridden features. Please
# note if you have multilingual site and
# site active language is not english then
# you have to replace word "Overridden" by
# it's translation.
drush fl | grep Overridden

# Show diff of overridden feature by name.
drush fd feature_name

# Update feature by name. Put changes from
# database into a code.
drush fu feature_name

# Revert feature by name. Restore database
# state from a code.
drush fr feature_name

Sql interface

# Show information about current database connection
# that defined in settings.php file.
drush sql-connect

# Login into sql console interface.
drush sqlc

# Execute a query.
drush sqlq "SELECT * from node;"

# Restore database dump from dump_name.sql file.
drush sqlc < dump_name.sql

# Save database dump into dump_name.sql.gz file.
drush sql-dump --gzip --result-file=dump_name.sql.gz

Other

# Run custom php code.
drush eval "variable_set('site_name', 'Test site name');"

# Get all dblog messages in realtime (tail style).
# Useful when you want to debug a remote Drupal
# installation where you have ssh access.
drush ws --tail

Key notes:

Jan 24 2017
Jan 24
Drupal provides a powerfull API for building different kind of forms. One of the most cool thing in this API, I think, it's a #states feature. It allows developers to create form elements that can change their state depending on some conditions. For instance you can create a text input which will be visible only if checkbox is checked. Or even make multiple conditions for an element.

Using this feature you can create OR, AND and XOR conditions. Let's consider an example. We have a form with two checkboxes and one text input:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
  ];

  return $form;
}
Let's say we want to show text_input_1 only if check_box_1 is checked. This example shows a SIMPLE CONDITION:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    // #states array describes element states depends on conditions.
    '#states' => [
      // State: this input will be visible only if checkbox_1 is checked.
      'visible' => [
        // Condition: selector of a checkbox this input depends on.
        'input[name="checkbox_1"]' => [
          // Condition value.
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
A complete list of existing states and condition values you can find here. Next example is AND CONDITION: let's show text_input_1 only if checkbox_1 AND checkbox_2 are checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // Just provide a few conditions in one state array.
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
        'input[name="checkbox_2"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
OR CONDITION. In this case we will show text_input_1 only if checkbox_1 OR checkbox_2 is checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // OR condition: provide a few conditions each wrapped into a
        // state array.
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
And a final example: XOR CONDITION. Show text_input_1 only if checkbox_1 OR checkbox_2 is checked but not both:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        'xor',
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
Of course you can combine conditions and make them really complex.

Be careful with 'required' state


If you make field required depends on a condition you have to a validate this case on a back-end side manually:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  // If checkbox is checked then text input
  // is required (with a red star in title).
  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'required' => [
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  $form['actions'] = [
    'submit' => [
      '#type' => 'submit',
      '#value' => t('Submit'),
    ],
  ];

  return $form;
}

/**
 * Form validate callback.
 */
function module_form_validate($form, $form_state) {
  // if checkbox is checked and text input is empty then show validation
  // fail message.
  if (!empty($form_state['values']['checkbox_1']) &&
    empty($form_state['values']['text_input_1'])
  ) {
    form_error($form['text_input_1'], t('@name field is required.', [
      '@name' => $form['text_input_1']['#title'],
    ]));
  }
}

Key notes:

Jan 21 2017
Jan 21
You have to cache results of heavy functions, sql queries and markup if it's possible because it reduces load on a system in general. If you have a block which renders a lot of items shiped from a database - cache it. If you have a function that performs heavy calculations and this function is called many times during a script execution - cache a result of this function.

Static cache


It's recommended to implement static cache for functions that are called many times during a script execution. It's easy to do: just store a function result first time when function is called and skip heavy calculation next time and return saved value from a static variable.
<?php

/**
 * Some function performs heavy calculations.
 *
 * @return mixed
 *   A result.
 */
function module_heavy_function() {
  // Function drupal_static() returns
  // a static variable by name. Usually
  // variable name equals a function
  // name.
  $result = &drupal_static(__FUNCTION__);

  if (!isset($result)) {
    // Perform heavy calculation here.
    ...

    // After that assign result to a
    // static variable.
    $result = $result_of_heavy_calculations;
  }

  return $result;
}

Database cache


If you want to save a function result for some period of time you have to implement database cache. You can specify a cache lifetime (when it will be expired) or invalidate cache manually when do you need.
<?php

/**
 * Some function performs heavy calculations.
 *
 * @return mixed
 *   A result.
 */
function module_heavy_function() {
  $cache_name = 'module:module_heavy_function';
  $result_of_heavy_calculations = NULL;

  // Cache miss: If there is no cached data or
  // cache have expired or you invalidated cache
  // manually do heavy calculations and save
  // a result into a cache.
  if (!cache_get($cache_name)) {
    // Perform heavy calculation here.
    ...

    // After that set cache. Data will be saved
    // into cache bin 'cache' for one hour.
    cache_set($cache_name, $result_of_heavy_calculations, 'cache', time() + 60 * 60);
  }
  // Cache hit: load cached data from a database
  // and return it.
  else {
    $cache = cache_get($cache_name);
    $result_of_heavy_calculations = $cache->data;
  }

  return $result_of_heavy_calculations;
}
Manual cache invalidating:
<?php

// You can invalidate all cached data
// that matches a wildcard cid 'module:*';
cache_clear_all('module:', 'cache', TRUE);

// Or you can invalidate cache by specific
// cid.
cache_clear_all('module:content', 'cache');
You can also cache a data of a renderable array by specifying #cache key.
<?php

$renderable_array['content'] = [
  '#cache' => [
    'cid' => 'module:content',
    'bin' => 'cache',
    'expire' => time() + 60 * 60,
  ],
  '#markup' => 'This markup will be cached',
];

Key notes:

Jan 15 2017
Jan 15
I've already written about how to use native Drupal ajax mechanism on front-end side in this post. There I've described how to send requests immediately or by clicking (or by any other event) on any DOM element. It works when javascript is enabled in user's browser. But how to make your menu callback work both with ajax and GET (when js is disabled) requests?

1. Menu router. Have to contain additional argument %ctools_js:
<?php

/**
 * Implements hook_menu().
 *
 * Just for example we will render a node form.
 */
function module_menu() {
  $items = [];

  $items['some/%node/%ctools_js/path'] = [
    'title' => 'Test router',
    'page callback' => 'module_test_router_callback',
    'page arguments' => [1, 2],
    // For ajax requests first.
    'delivery callback' => 'ajax_deliver',
    'type' => MENU_CALLBACK,
  ];

  return $items;
}
2. Menu callback:
<?php

/**
 * Menu callback for test router.
 *
 * Just for example we will render a node form
 * and if it's ajax request then replace body element
 * with a form. Otherwise reload page and show
 * node form.
 */
function module_test_router_callback($node, $ajax) {
  // Don't forget to include node specific file
  // for an example.
  module_load_include('inc', 'node', 'node.pages');
  $form = drupal_get_form($node->type . '_node_form', $node);

  // If it's ajax request then $ajax will be equals 1.
  if ($ajax) {
    // Set up command for replacing body content
    // with a rendered form.
    $commands[] = ajax_command_html('body', render($form));

    return [
      '#type' => 'ajax',
      '#commands' => $commands,
    ];
  }
  else {
    // If it's not ajax request just deliver
    // html markup.
    drupal_deliver_html_page($form);
  }
}
3. Link with a use-ajax class:
<?php

// Make sure you've added ajax library
// before printing a link.
drupal_add_library('system', 'drupal.ajax');

// Main point here is 'use-js' class for a link.
// Also take a look at 'nojs' argument. If js is
// enabled in a browser then this string will
// be replaced with 'ajax' and link will be look
// like 'some/NID/ajax/path'.
print l(t('Get node form'), 'some/NID/nojs/path', [
  'attributes' => [
    'class' => [
      'use-ajax',
    ],
  ],
]);
So what's happening here?

  • First. Clicking by the link with 'some/NID/nojs/path' href and 'use-ajax' class. If js is enabled in user's browser then 'nojs' substring will be replaced by 'ajax'.
  • Second. You need to find out what is %ctools_js. Ctools module provides a function ctools_js_load() that determines if it's ajax request or not ('ajax' or 'nojs' string passed as an argument to page callback).
  • Third. Page callback takes argument $ajax which equals 1 if ajax request and 0 otherwise (thanks to ctools_js_load() function). We're returning ajax commands for ajax requests and delivering render array with drupal_deliver_html_page() function otherwise. Since we've defined delivery callback for menu router as ajax_deliver we need to handle non ajax requests with drupal_deliver_html_page() function manually.

Key notes:

Jan 13 2017
Jan 13
Default entities (node, user, term etc) in Drupal 7 are instances of a stdClass class. It means you can't determine entity type directly. Let's say you have an object and you want to ensure that it's a node and only then do something node specific. Entity API doesn't provide such possibility out of the box.

When it can be helpful? Well, real life example: custom ctools context plugin which returns some value depends on an object loaded from current panel's url.
<?php

/**
 * API function for context determination.
 */
function module_get_context() {
  ...

  // Load object from current page (page manager).
  // Path could be node/%node, user/%user or term/%term.
  $current_menu_object = menu_get_object('pm_arg', 1);

  // Is it a node? If yes we need to do one operation.
  // If it's a user we need to do something else.
  $object = $current_menu_object->data;

  ...
}
To solve this problem you can use next functions:
<?php

/**
 * API function to detect if passed object is a node.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's a node FALSE otherwise.
 */
function module_is_node($object) {
  return !empty($object->nid);
}

/**
 * API function to detect if passed object is a term.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's a term FALSE otherwise.
 */
function module_is_term($object) {
  return !empty($object->tid);
}

/**
 * API function to detect if passed object is an user.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's an user FALSE otherwise.
 */
function module_is_user($object) {
  return !empty($object->uid);
}
I usually put helper functions into separate module that I create for each project.
So having this functions now you can determine entity type and do some specific actions:
<?php

/**
 * API function for context determination.
 */
function module_get_context() {
  ...

  // Load object from current page (page manager).
  // Path could be node/%node, user/%user or term/%term.
  $current_menu_object = menu_get_object('pm_arg', 1);

  // Is it a node? If yes we need to do one operation.
  // If it's a user we need to do something else.
  $object = $current_menu_object->data;

  if (module_is_node($object)) {
    // Do node specific actions here.
  } elseif (module_is_term($object)) {
    // Do term specific actions here.
  }

  ...
}
Of course you can extend this function set for other entities like file, etc. Or even write one universal function for checking all entity types.

Key notes:

Jan 10 2017
Jan 10
Drupal 7 offers a mechanism to render entities which called view mode. For example you can set up how your entity will look as a full page or as a teaser. By default Drupal provides only two modes for node entity (default and teaser) and one default mode for taxonomy term and user entities. But you can easily add your custom view mode.

First option is setup display suite module. It's powerfull and provides a lot of features like a layout for entity that can be set up from admin UI. But if you need just to add a view mode to an entity and don't need any other functionality you can do it manually.

In this example we will add a new view mode to a node entity:

<?php

/**
 * Implements hook_entity_info_alter().
 */
function module_entity_info_alter(&$entity_info) {
  $entity_info['node']['view modes']['custom_view_mode'] = [
    'label' => t('Custom view mode title'),
    // Use default display settings from `default` view mode.
    // It means that all fields labels settings and fields
    // formatters settings will be exactly the same as in
    // `default` view mode.
    'custom settings' => FALSE,
  ];
}
Now clear the cache and you will get a new view mode.

Basically that's all. You can set up fields formatters, weights and etc. But you might want to assign a specific template for nodes per view mode. Well it's not hard to do. Just define a hook_preprocess_node in theme's template.php file:

<?php

/**
 * Implements theme_preprocess_node().
 */
function theme_preprocess_node(&$variables) {
  $variables['theme_hook_suggestions'][] = 'node__' . $variables['type'] . '__' . $variables['view_mode'];
}
Then define node--node-type--view-mode.tpl.php template in your theme folder and provide needed markup there. I suggest to get the default markup from node.tpl.php file and modify it. Clear the cache once again and you will get a rendered through defined template node.

Key notes:

Jan 08 2017
Jan 08
Throbber is an element with a message that shows us that something is running in background. Drupal renders this element each time when ajax request is running (for example when you submit form via ajax). Sometimes we need to disable throbber at all or at least remove/change message. That's how you can do this.

Disable throbber at all:
<?php

// Add 'progress' settings into '#ajax'
// element of a submit button. Define progress
// type as 'none'.
$form['actions']['submit']['#ajax']['progress'] => [
  'type' => 'none',
];
Remove/change throbber message:
<?php

// Add 'progress' settings into '#ajax'
// element of a submit button. Change progress
// message to NULL (delete message) or any other
// string (change message).
$form['actions']['submit']['#ajax']['progress'] => [
  'message' => NULL,
];
If you  send ajax requests as described in this post you should modify settings variable and add progress property. Disable throbber:
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  progress: {
    type: 'none'
  }
});
Remove/change throbber message:
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  progress: {
    type: 'throbber',
    // Change progress message to NULL
    // (delete message) or any other
    // string (change message).
    message: null
  }
});

Key notes:

Jan 08 2017
Jan 08
If you are using jQuery.ajax() or jQuery.post() for background requests you should have a look at native Drupal ajax implementation. Drupal provides a js object Drupal.ajax that performs all needed actions for you (like processing ajax commands that came from back-end side). All that you need to do is to create instance of this object and configure it.

Send ajax request by event triggering of a DOM object:
// By clicking on this link we will
// send an ajax request.
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  // Pass some data to back-end side
  // if you need.
  submit: {
    some_data: 'some_data_value'
  }
});
Send ajax request immediately:
// If we want to send request immediately
// we can omit first two parameters.
var ajax = new Drupal.ajax(false, false, {
  url: '/ajax/callback/path'
});

// To send request call this function.
// You shouldn't think how to process
// response. Be sure that all commands
// will be processed correctly.
ajax.eventResponse(ajax, {});

Key notes:

Jan 07 2017
Jan 07
Drupal 7 offers a lot of default ajax commands which are the part of Drupal Ajax Framework. Furthermore, ctools module extends a set of ajax commands and provides own commands. But what if you need some specific command? First, you need to write a wrapper function which returns ajax command array. Should looks like this one:
<?php

/**
 * Ajax command for rendering canvas data.
 *
 * @param array $data
 *   Settings array.
 *
 * @param mixed $some_other_parameter
 *   Some other parameter.
 *
 * @return array
 *   Ajax command array.
 */
function specific_ajax_command(array $data, $some_other_parameter) {
  return [
    // Name of a js function.
    'command' => 'specificAjaxCommand',

    // Data will be available in js function.
    'specific_data' => $data,

    // You can define as many parameters
    // for your command as you want.
    'some_other_parameter' => $some_other_parameter,
  ];
}
Second step is js command function implementation :
(function ($) {
  'use strict';

  // Ensure that Drupal.ajax object is set up.
  if (Drupal.ajax) {
    Drupal.ajax.prototype.commands.specificAjaxCommand = function (ajax, response, status) {
      // Check response status.
      if (status == 'success') {
        // Get data from back-end side.
        var specificData = response.specific_data,
            someOtherParameter = response.some_other_parameter;
        
        // Perform specific actions dependent on a data.
      }
    };
  }

}(jQuery));
Third, ensure that js file contains code above is added to a page before returning ajax command. When it's done you are ready to go with a new ajax command:
<?php

/**
 * Ajax page callback.
 *
 * @return array
 *   Array contains Drupal ajax commands.
 */
function module_ajax_page_callback() {
  $some_data = [];
  $some_other_parameter = 'Some other parameter value';
  $commands[] = specific_ajax_command($some_data, $some_other_parameter);
 
  return [
    '#type' => 'ajax',
    '#commands' => $commands,
  ];
}

Key notes:

Jan 07 2017
Jan 07
If you want to submit node form (or any form) via ajax request you need to follow next steps:
  1. Alter needed form: add container for validation messages and ajax callback for submit action.
  2. Implement ajax callback that returns ajax commands.
<?php

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function module_form_node_form_alter(&$form, &$form_state, $form_id) {
  // Add container for validation messages.
  $form['#prefix'] = '
';

  // Add ajax callback for submit action.
  $form['actions']['submit']['#ajax'] = [
    'callback' => 'module_node_form_ajax_submit',
  ];

  // Don't forget to include node.pages.inc file. Without it Drupal will not be
  // able to find and call node_form_validate() function.
  form_load_include($form_state, 'inc', 'node', 'node.pages');
}

/**
 * Form ajax submit callback.
 *
 * @return array
 *   Array contains Drupal ajax commands.
 */
function module_node_form_ajax_submit() {
  // Remove all status messages from $_SESSION['messages'] variable
  // because we want to render only error messages.
  drupal_get_messages('status');

  // Check form for possible validation messages.
  if (form_get_errors()) {
    // Validation failure: return rendered validation messages.
    $commands[] = ajax_command_html('#validation-messages', [theme('status_messages')]);
  }
  else {
    // Validation success: do something, for instance redirect user.
    ctools_include('ajax');
    $commands[] = ctools_ajax_command_redirect('some/page');
  }

  return [
    '#type' => 'ajax',
    '#commands' => $commands,
  ];
}

Key notes:

Jan 07 2017
Jan 07
If you have redirect functionality implemented with drupal_goto() function on hook_init() probably you aren't able to run drush commands because they crashe with a message:
Drush command terminated abnormally due to an unrecoverable error.  [error]
It happens because drush bootstraps Drupal application but can't perform redirect. To avoid this you need to check whether script runs from cli or not:
<?php

/**
 * Implements hook_init().
 */
function module_init() {
  if (!drupal_is_cli()) {
    drupal_goto('some/path');
  }
}

Key notes:

Jan 07 2017
Jan 07
When you build sql select queries using Drupal database API often you need to debug them. I mean to copy sql string, paste into sql editor, run it and see a result. If you cast query object to string you simply get a sql query with placeholders instead of their values. To get a string with replaced placeholders by values you can use this function:
<?php

/**
 * Helper function returns sql string for db_select object.
 *
 * @param \SelectQueryInterface $query
 *   Select query object.
 *
 * @return string
 *   SQL string with replaced parameters.
 */
function module_get_select_query_string(SelectQueryInterface $query) {
  $string = (string) $query;
  $db = Database::getConnection();
  $string = str_replace(['{', '}'], [
    '`' . $db->tablePrefix(),
    '`',
  ], $string);
  $arguments = $query->arguments();

  if (!empty($arguments) && is_array($arguments)) {
    foreach ($arguments as $placeholder => &$value) {
      if (is_string($value)) {
        $value = "'$value'";
      }
    }

    $string = strtr($string, $arguments);
  }

  return $string;
}
You can also use devel's dpq() function but I prefer have a small helper module with needed functions only. Usage example:
<?php

$query = db_select('node', 'n')
  ->condition('status', 1)
  ->fields('n', ['nid']);

print module_get_select_query_string($query);

Key notes:

Aug 21 2016
Aug 21
TL;DR The Google Summer of Code period ends, and am glad that I am able to meet all the goals and develop something productive for the Drupal community. In this blog post, I will be sharing the details of the project, the functionality of the module and its current status.

I am glad that I was one of the lucky students who were selected to be a part of the Google Summer of Code 2016 program for the project “Integrate Google Cloud Vision API to Drupal 8”. The project was under the mentorship of Naveen Valecha, Christian López Espínola and Eugene Ilyin. Under their mentoring and guidance, I am able meet all the goals and develop something productive for the Drupal community.

Let me first share why the Google Vision API module may be required.

Google Cloud Vision API bring to picture the automated content analysis of the images. The API can not only detect objects ranging from animals to famous monuments, but also detects faces on emotions. In addition, the API can also help censor images, extract text from images, detect logos and landmarks, and even the attributes of the image itself, for instance the dominant color in the image. Thus, it can serve as a powerful content analysis tool for images.

Now let us see how can we put the module to use, i.e. what are its use cases. To start with, the Google Vision API module allows Taxonomy tagging of image files using Label Detection. Label Detection classifies the images into a number of general purpose categories. For example, classifying a war scenario to war, troop, soldiers, transport, etc. based on the surroundings in the images. This feature of the module is especially important to filter the images based on some tags.

Second feature listing our use case is the Safe Search Detection. It quickly identifies and detects the presence of any explicit or violent contents in an image which are not fit for display. When this feature is enabled in the module, the Safe Search technique validates any image for explicit/violent contents. If found, these images are asked for moderation, and are not allowed to be uploaded on the site, thus keeping the site clean.

Please click here for video demonstration of the two above-mentioned use cases.

Continuing with the other use cases, the third one is Filling the Alternate Text field of an image file. Label, Logo, Landmark and Optical Character Detection feature of the Google Cloud Vision API have been used to implement this use case. Based on the choice entered by the end user, he/she can have the Alternate Text for any image auto filled by one of the four above-mentioned options. The choice “Label Detection” would fill the field with the first value returned in the API response. “Logo Detection” identifies the logos of famous brands, and can be used to fill the field accordingly. Likewise, “Landmark Detection” identifies the monuments and structures, ranging from natural to man-made; and “Optical Character Detection” detects and identifies the texts within an image, and fills the Alternate Text field accordingly.

Next comes the User Emotion Detection feature. This feature is especially important in cases of new account creation. On enabling this feature, it would detect the emotion of the user in the profile picture and notify the new user if he/she seems to be unhappy in the image, prompting them to upload a happy one.

Lastly, the module also allows Displaying the similar image files. Based on the dominant color component (Red, Green or Blue), the module quickly groups all the images which share the same color component, and display them under the “Similar Content” tab in the form of a list. Each item links itself to the image file itself, and is named as per the filename saved by the user. Users should note here that by “similar contents”, we do not mean that the images would resemble each other always. Instead we mean here the same dominant color components.

All the details of my work, the interesting facts and features have been shared on the Drupal Planet.

Please watch this video to know more on how to use the above-mentioned use cases in proper way.

[embedded content]


This is the complete picture of the Google Vision API module developed during the Google Summer of Code phase (May 23, 2016- August 23, 2016).

With this, the three wonderful months of Google Summer of Code phase comes to an end, enriching me with lots of experiences, meeting great persons and working with them. In addition of giving me an asset, it also boosted and enhanced my skills. I learnt a lot of new techniques, which probably, I would not have learnt otherwise. The use of services and dependency injection, constraints and validators, controllers, automated tests and the introduction to concepts of entities and entity types to name a few.
I would put to use these concepts in best possible way, and try to contribute to the Drupal community with my best efforts.
Aug 16 2016
Aug 16
TL;DR Last week I had worked moving the helper functions for filling Alt Text of image file to a new service; and moving the reused/supporting functions of the tests to an abstract parent class, GoogleVisionTestBase. This week I have worked on improving the documentation of the module and making the label detection results configurable.

With all major issues and features committed to the module, this week I worked on few minor issues, including the documentation and cleanup in the project..

It is an immense pleasure for me that I am getting the feedbacks from the community on the Google Vision API module. An issue Improve documentation for helper functions was created to develop more on documentation and provide the minute details on the code. I have worked on it, and added more documentation to the helper functions so that they can be understood better.

In addition, a need was felt to let the number of results obtained from the Vision API for each of the feature as configurable, and allow the end user to take the control on that. The corresponding issue is Make max results for Label Detection configurable. In my humble opinion, most of the feature implementations and requests to the Google Cloud Vision API have nothing to do with allowing the end user to configure the number of results. For instance, the Safe Search Detection feature detects and avoids the explicit contents to be uploaded, and does not need the number of results to be configurable. However, the taxonomy tagging using Label Detection should be user dependent, and hence, I worked on the issue to make the value configurable only for Label Detection purpose. This value can be configured from the Google Vision settings page, where we set the API key. I have also developed simple web tests to verify that the value is configurable. Presently, the issue is under review.

I have also worked on standard coding fixes and pa-reviews and assisted my mentor, Naveen Valecha to develop interfaces for the services. I assisted him on access rights of the functions, and fixing the documentation issues which clashed with the present one.

Lastly, I worked on improving the README and the module page to include all the new information and instructions implemented during the Google Summer of Code phase.

With all these works done, and all the minor issues resolved, I believe that the module is ready for usage with all the features and end user cases implemented.
Next Week, I’ll work on creating a video demonstration on how to use Google Vision API to fill the Alt Text attribute of an image file, detect the emotion in the user profile pictures and to group the similar images which share the same dominant color.
Aug 09 2016
Aug 09
TL;DR Last week I had worked on modifying the tests for “Fill Alt Text”, “Emotion Detection” and “Image Properties” features of the Google Vision API module. The only tasks left are moving the supporting functions to a separate service, in addition to, creating an abstract parent class for tests and moving the functions there.

The issues Alt Text field gets properly filled using various detection features, Emotion Detection(Face Detection) feature and Implementation of Image Properties feature of the Google Vision API module are still under review by my mentors. Meanwhile, my mentors asked me to move the supporting functions of the “Fill Alt Text” issue to a separate service and use it from there. In addition, they also suggested me to create an abstract parent class for the Google Vision simple tests, and move the supporting functions to the parent class. Thus, this week, I contributed to follow these suggestions and implement them out.

There are few supporting functions, namely, google_vision_set_alt_text() and google_vision_edit_alt_text() to fill the Alt Text in accordance to the feature requested from the Vision API, and also to manipulate the value, if needed. I moved these functions to a separate service, namely, FillAltText, and have altered the code to use the functions from there instead of directly accessing them.

In addition, there are a number of supporting functions used in the simple web tests of the module, to create users, contents and fields, which were placed in the test file itself, which in one way, is a kind of redundancy. Hence, I moved all these supporting functions to abstract parent class named GoogleVisionTestBase, and altered the test classes to extend the parent class instead and in place of WebTestBase. This removed the redundant code, as well as, gave a proper structure and orientation to the web tests.
These minor changes would be committed to the module directly, once the major issues are reviewed by my mentors and committed to the module.
Aug 03 2016
Aug 03
TL;DR Last week, I had worked on and developed tests to ensure that the Alt Text field of an image file gets filled in accordance to the various detection features of the Vision API, namely Label Detection, Landmark Detection, Logo Detection and Optical Character Detection. This week I have worked to modify and add tests to various features of the Google Vision module, namely filling of Alt Text field, emotion detection of user pictures and grouping the image files on the basis of their dominant color component.

My mentors reviewed the code and the tests which I had put for review to get them committed to the Google Vision API module. However, the code needs some amendment pointed out by my mentors, which was to be corrected before commit. Hence, I spent this week working on the issues and resolving the flaws, rather than starting with a new feature. Let me start discussing my work in detail.

I had submitted the code and the tests which ensure that the Alt Text field gets properly filled using various detection features according to the end user choice. However, as was pointed out by my mentor, it had one drawback- the user would not be able to manipulate or change the value of the field if he wishes to. Amidst the different options available to the end user to fill the alt text field of the image file, there was a small bug- once an option is selected, it was possible to switch between the options, however, disabling it was not working. After, been pointed out, I worked on modifying the feature and introducing the end user ability to manipulate the value of the field as and when required. Also, I worked on the second bug, and resolved the issues of disabling the feature.

Regarding the Emotion Detection(Face Detection) feature of the Vision API, I was guided to use injections instead of using the static methods directly, and to modify variables. For example, the use of get(‘entity_type.manager’) over the static call \Drupal::entityTypeManager(). Apart from these minor changes, a major issue was the feature was being called whenever an image file is associated with. However, I need to direct it to focus only when the user uploads an image, and not on its removal (as both the actions involves an image file, hence the bug).

In the issue, Implementation of Image Properties feature in the Vision API, I had queried multiple times to the database in the cycle to fetch results and build the routed page using the controllers. However, my mentor instructed me that its really a bad way of implementing the database queries to fetch the results. Hence, I modified the code and changed them to single queries to fetch the result and use them to build the page. In addition, I was asked to build the list using ‘item_list’ instead of using the conventional ‘#prefix’ and ‘#suffix’ to generate the list. Another important change in my approach towards my code was the use of db_query(), the use of which is deprecated. Hence, I switched to use addExpressions() instead of db_query().
Presently, the code is under review by the mentors. I will work further on them, once they get reviewed and I get further instructions on it.
Jul 27 2016
Jul 27
TL;DR Last week, I had worked on and developed tests to ensure that the similar images are grouped in accordance to the Image Properties feature of the Vision API. The code is under review by the mentors, and I would continue on it once the review is done. Meanwhile, they also reviewed the “Fill Alt Text” feature issue, and approved it is good to go. This week, I have worked on developing tests for this issue.

An important feature that I have implemented in the Google Vision API module is the filling of Alt Text field of an image file entity by any of the four choices- Label Detection, Landmark Detection, Logo Detection and Optical Character Detection. My mentor suggested me to check the availability of the response and then fill the field, as we can not fully rely on the third party responses. With this minor suggestion being implemented, now its time to develop tests to ensure the functionality of this feature.

I started developing simple web tests for this feature, to ensure that the Alt Text field is properly filled in accordance to the choice of the user. It requires the selection of the four choices one by one and verify that the field is filled correctly. Thus we require four tests to test the entire functionality. I have added an extra test to ensure that if none of the options are selected then the field remains empty.

I created the image files using the images available in the simpletests. The images can be accessed through drupalGetTestFiles(). The filling, however, requires call to the Google Cloud Vision API, thus inducing dependency on the API key. To remove the dependency, I mocked the function in the test module, returning the custom data to implement the feature.

The first test ensures that the Label Detection feature returns correct response and the Alt Text field is filled correctly. The simpletest provides a list of assertions to verify it, however, I found assertFieldByName() to be most suitable for the purpose. It asserts the value of a field based on the field name. The second test ensures that the Landmark Detection feature works correctly. Similarly, the third and fourth test ensures the correct functionality of the Logo and the Optical Character Detection feature.

The fifth test which I have included perform tests when none of the options are selected. It ensures that under this case, the Alt Text field remains empty, and does not contain any unwanted values.
I have posted the patch covering the suggestions and tests on the issue queue Fill the Alt Text of the Image File using Google Vision API to be reviewed by my mentors. Once they review it, I would work on it further, if required.
Jul 26 2016
Jul 26
TL;DR In the past two weeks I had worked on using the Image Properties feature offered by the Google Cloud Vision API to group the image files together on the basis of the dominant color components filling them. In addition, I had also worked on detecting the image files and filling the Alternate Text field based on the results of Label/Landmark/Logo/Optical Character Detection, based on the demand of the end user. This week, I have worked on and developed tests to ensure that the similar images are grouped in accordance to the Image Properties feature of the Vision API.

At present, the Google Vision API module supports the Label Detection feature to be used as taxonomy terms, the Safe Search Detection feature to avoid displaying any explicit contents or violence and the User Emotion detection to detect the emotions of the users in their profile pictures and notify them about it.

I had worked on grouping the images on the basis of the dominant color component(Red, Green or Blue) which they are comprised of. I got the code reviewed by my mentors, and they approved it with minor suggestions on injecting the constructors wherever possible. Following their suggestions, I injected the Connection object instead of accessing the database via \Drupal::database().

After making changes as per the suggestions, I started developing simple web tests for this feature, to ensure that the similar images gets displayed under the SImilarContents tab. It requires the creation of new taxonomy vocabulary and adding an entity reference field to the image file entity. After the creation of the new Vocabulary and addition of the new field to the image file, I created the image files using the images available in the simpletests. The images can be accessed through drupalGetTestFiles(). The first test ensures that if the Vocabulary named ‘Dominant Color’ is selected, the similar images gets displayed under the file/{file_id}/similarcontent link.

The grouping, however, requires call to the Google Cloud Vision API, thus inducing dependency on the API key. To remove the dependency, I mocked the function in the test module, returning the custom data to implement the grouping.

To cover the negative aspect, i.e. the case when the Dominant Color option is not selected, I have developed another test which creates a demo vocabulary to simply store the labels, instead of the dominant color component. In this case, the file/{file_id}/similarcontent link displays the message “No items found”.
I have posted the patch covering the suggestions and tests on the issue queue to be reviewed by my mentors. Once they review it, I would work on it further, if required.
Jul 14 2016
Jul 14
TL;DR Previous week I had worked on detecting the emotion in the profile pictures of the users, and notifying them to change the image if they do not look happy. The work is under review by the mentors. Once it gets reviewed, I would resume it if it needs any changes. This week I have worked on filling the ‘Alt Text’ field of an image file based on any one of the method selected by the end user- Label Detection, Landmark Detection, Logo Detection and Optical Character Detection.

Last week, I had worked on implementing the Face Detection feature in the Google Vision API module. The code is currently under the review by the mentors. Once, they review it, I would develop further on it if it requires any changes.

The Google Cloud Vision API provides the features to detect popular landmarks in an image(Landmark Detection), logos of popular brands(Logo Detection), texts within an image(Optical Character Detection), in addition to Label Detection. These features, though of less significance, are helpful in identifying an image. Hence, I have started working on implementing a new helpful case for the users- Filling of the Alternate Text field of an image file using these features.

The Alt Text field of the image file entity is modified to incorporate the options to fill the field using the features. The user may select any one of the four options to fill the Alt Text field of the image.

Coming to the technical aspect, I have made use of hook_form_BASE_FORM_ID_alter() to alter the Alternate Text field of the image file entity. I have modified the edit form of the Alt Text field to add four radio options, namely- Label Detection, Landmark Detection, Logo Detection and Optical Character Detection. The user may select any of the options and save the configuration. The Alternate Text field would be filled up accordingly.
Presently, the code is under the review by the mentors. Once it gets reviewed, I would make suggested changes, if required.
Jul 07 2016
Jul 07
TL;DR Previous week I had worked on grouping the contents based on the dominant color component in their images, if present. The work is under review of the mentors. And once, it gets reviewed, I would work further on that issue. Meanwhile, I have started developing and implementing the Emotion Detection feature of the Google Cloud Vision API. It would detect the emotion of the person in the profile picture uploaded, and if the person looks angry or unhappy, he would be notified thereof. This feature is especially important when building sites for professional purposes, as the facial looks matters a lot in such cases.

Last week, I had worked on implementing the Dominant Color Detection feature in the Google Vision API module. The code is currently under the review by the mentors. Once, they review it, I would develop further on it if it requires any changes.

Hence, meanwhile, I have started working on implementing a new feature Face Detection in an image. This feature gives us the location of the face in an image, and in addition, the emotions and expressions on the face.

I have used this feature to detect the emotion of the person in the profile picture uploaded by him/her. If the person does not seem happy in the image, he/she is notified thereof of their expressions. This is especially useful when the end users are developing a site for professional purposes, as in professional matters, expressions matters a lot.

Coming to the technical aspect, I have made use of hook_entity_bundle_field_info_alter() to alter the image fields, and check the emotions in the uploaded images. This function has been used, as we only want to implement this feature on the image fields. If the image is not a happy one, then appropriate message is displayed using drupal_set_message(). This feature also makes use of Constraints and Validators just like the Safe Search detection feature. Presently, the code is under the review by the mentors.

In addition to the implementation of Face Detection, I also worked on expanding the tests of the Safe Search Detection feature of the Google Vision API module to test other entities as well, in addition to the nodes. I have expanded the tests to test the safe search constraint on the comment entity as well. This requires the creation of a dummy comment type, adding an image field to the comment type, and attaching the comment to the content type. The image field contains the safe search as the constraint on it. This test is basically similar to the tests present in the module for the node entity. The code is under review by the mentors and would soon be committed to the module. For reference on how to create dummy comment types and attaching it to the content types, the CommentTestBase class is very helpful.
Jun 29 2016
Jun 29
TL;DR The safe search constraint feature is now committed to the module along with proper web tests. So, this week I started off with a new feature offered by the Google Cloud Vision API- “Image Properties Detection”. It detects various properties and attributes of an image, namely, the RGB components, pixel fraction and score. I have worked on to detect the dominant component in the image present in any content, and display all the contents sharing similar dominant color. It is pretty much like what we see on the e-commerce sites.

Previous week I had worked on writing web tests for the safe search constraint validation on the image fields. This feature is now committed in the module Google Vision API.

This week I have worked on implementing another feature provided by the Google Cloud Vision API, namely, Image Properties Detection. This feature detects the color components of red, green and blue colors in the images along with their pixel fractions and scores. I have used this feature to determine the dominant color component (i.e. red, blue or green) in the image, and to display all those contents which have the same dominant color in their images.

I have developed the code which creates a new route- /node/{nid}/relatedcontent to display the related contents in the form of a list. This concept makes use of Controllers and Routing System of Drupal 8. The Controller class is extended to render the output of our page in the format we require. The contents are displayed in the form of list with the links to their respective nodes, and are named by their titles.

In addition to the grouping of similar contents, the colors are also stored in the form of taxonomy terms under a taxonomy vocabulary programmatically generated under the name Dominant Colors.

This issue is still under progress, and requires little modification. I need to add the link to the new route in each of the nodes, so as to  get a better interface to access those contents. Henceforth, I will put this patch for review.
A very simple example of creating routes and controllers in your module can be found here.
Jun 29 2016
Jun 29
TL;DR It has been over a month since I started working on my Drupal project “Integrate Google Cloud Vision API to Drupal 8”, and gradually I have crossed the second stage towards the completion of the project, first being selection in the Google Summer of Code 2016 programme. Here, I would like to share my experiences and accomplishments during this one month journey, and also I would like to summarize my further plans with the project and the features which I would be implementing in the coming two months.

Let me first describe the significance of this post and what actually does “midterm submission” means? The GSOC coding phase has been divided into two halves, viz. Midterm submission and Final submission. In the first half, the students try to accomplish around 50% of the project, and submit their work to the mentors for evaluation. Those who passed the midterm evaluations are allowed to proceed further and complete the remaining portion of their project.

Now coming back to my experiences, after successfully passing through the Community Bonding period of the GSOC 2016 programme, now it was the time for start coding our project proposal to reality. As I had shared earlier that during the Community Bonding period, I came to know that the project has already been initiated by Eugene Ilyin,(who is now a part of my GSOC team). So, we discussed upon the project and set a roadmap of the project and the goals we need to achieve in the GSOC period. I had started coding the very first day of the coding phase, moving the new as well as existing functions to services. My mentors Naveen Valecha, Christian López Espínola and Eugene Ilyin really helped me a lot and guided me whenever and wherever I needed their guidance and support. They helped me to get through new concepts and guided me to implement them in the most effective way to make the module the best that we could.

During this period, I also came to learn about a lot of new techniques and concepts which I had not implemented earlier. Right from the very first day of the coding period, I have been coming across new things everyday, and it is really interesting and fun to learn all those techniques. In this one month period, I learnt about services and containers and how to implement them. The post on Services and dependency injection in Drupal 8 and the videos of Drupalize.me were of great help to understand the concept of services and implement dependency injection. I also learnt about the use of validators and constraints and how can they be implemented both on general basis or specifically on fields. I also learnt about how to create test modules and alter various classes and their functions in our tests so as to remove the dependency on web access or on valid informations for our testing purposes. I learnt new things every day and enjoyed implementing them to code our module plan into reality. At present, the module supports the Label Detection feature of the Vision API, along with the tests to verify whether the API key has been set by the end user or not. Currently, the feature of Safe Search Detection is available as a patch which can be found here, which would soon be committed to the module.
[embedded content] I have shared all the details of my work on the Drupal Planet. Please watch this video for detailed information on how to use the Google Vision API module on your Drupal site.
Jun 23 2016
Jun 23
TL;DR In my last post Avoid Explicit Contents in the images using Google Vision module, I had discussed about the services which “Safe Search” feature of the Vision API provides us, and how we have put this into use in the Google Vision module as a constraint to all the image fields which would be validated when the option is enabled. This week I have worked on developing simple web tests for testing this feature whether it gives us the results as expected.

Last week I had worked on developing the code to use the Safe Search detection feature as a constraint to the image fields which would validate the images for the presence of explicit contents, provided that the user enables the configuration for the concerned image field.

Besides the code, testing the functionality using simple web tests are equally essential to ensure that the feature executes perfectly when necessary steps are implemented. Hence, this week I have worked on developing simple web tests, which ensures that we have a fully functional feature.

I have tested both the conditions with safe search enabled and disabled to verify the results which should be obtained. When the safe search is enabled, any image containing any sort of  explicit content, is detected, and asked for moderation. If the image is not moderated, then the image is not saved. When the same image was passed through the second test, with safe search disabled, it was stored successfully, thus providing us the expected results.

To conduct the test successfully, I had to create a demo content type in my tests using drupalCreateContentType(), which would have an image field with the ‘Enable Safe Search’ option. This was something new to me to how to add an extra field to the default content type settings. The Drupal documentation on FieldConfig and FieldStorageConfig were of great help to understand the underlying concepts and functions which the field offers, and thus helping me to create custom fields programmatically. However, in order to perform the test, I had to call the API directly, which required a valid API key and an image which actually contains explicit content. Hence, my mentors asked me to override the functions of the class (mocking the services) in such a way that it removes the dependency from both the key and the image. Thus, I created a test module inside the Google Vision module, and override the function.

Summarizing the above, I can say that in addition to learning how to test the constraints and validators, I also came to learn about some really cool techniques, including the creation of custom fields in tests and mocking the services.
The lingotek_test of the Lingotek Translation module is a good reference to learn about how to override the services in web tests. Other references which are useful for mocking are ServiceProviderBase and Mocking for Unit Tests.
Jun 14 2016
Jun 14
TL;DR Safe Search detection of the Google Cloud Vision API allows the end users to avoid any explicit content or violence in images to be uploaded on the site. I worked on integrating this feature to the module as a constraint to those image fields which have the “Safe Search” feature enabled.

Let me first give a brief summary about the current status of the module Google Vision. In the earlier weeks, I had implemented the services and wrote tests for the module, which are now committed to the module.

Now, coming to the progress of this week.

I had started with integrating the Safe Search detection feature in the module. Safe Search detection allows its users to detect any explicit contents within the image, and hence can be very useful for site administrators who do not want to display any explicit images on their sites.

This feature was initially integrated using a function call in the module. However, my mentors suggested that this feature should rather be a Constraint on the image fields, which would be validated if the feature is enabled for the field. It depends on the user whether to keep safe search on their site or not, and they can implement it any time they want by just enabling/disabling the checkbox on the image field. Hence, now it is a user choice, rather than the developer’s choice whether to implement this feature or not.

Presently, the code is under review by my mentors whether it needs changes or is ready for commit.

Constraints and Validators are wonderful features of Drupal 8. Constraints, as the name goes, are certain restrictions which we pose on the various fields. Validators are used to implement the logic to create the situation under which the constraints are to be applied. Some helpful examples of applying custom constraints and validators can also be found Sitepoint.
This week had a lot of new things stored for me. I had no idea about the constraints and validators when I was asked to implement them at the first place. I spent hours on them, learning about them and seeking guidance from my mentors on the issues I faced. I developed gradually on it, and by the end of the week, I was able to implement them for the safe search detection feature.

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web