Create Custom Contests with Drupal 8’s Voting API

Note: This blog post is NOT about Voting in Iowa and an App that failed to do its job. But if anyone wants an opinion on how that App should have been built and how much it should have cost, drop us a line 🙂

The great thing about the open source community around Drupal is the range of complex features that already exist. Sometimes, though, the documentation on how to use those pre-built pieces of functionality is lacking. That’s a situation we found ourselves in recently with Drupal’s Voting API.

We found a great tutorial for implementing the Voting API without any customization. Drupalize.Me also has helpful instructional video and text. But if you want to extend the module programmatically, there is very little material online to help guide the way.

The Problem to Solve

Oomph needed to launch a national crowd-sourcing contest for a long-time client. We had to provide visitors with a chance to submit content, and we needed to enable a panel of judges to vote on that content and determine a winner. But behind the scenes, we had to add functionality to enable voting according to specific criteria that our client wanted to enforce. For moderation, there would be an admin page that displays all entries, the scores from all judges, and the ability to search and sort.

Oh, and we had to turn it around in three months — maybe we should have led with that requirement. 😊

Architecting the Solution

Drupal 8 was a natural fit for rendering forms to the user and collecting the input for judging. A few contributed modules got us closer to the functionality we wanted — webformwebformcontentcreatorfivestarvotingapi, and votingapi_widgets.

The robust framework of the Voting API has been around since Drupal 4, so it was a great foundation for this project. It made no sense to build our own voting system when one with a stable history existed. Customizing the way that the API worked put up some roadblocks for our engineering team, however, and the lack of documentation around the API did not help. We hope that by sharing what we learned along the way, we can support the community that maintains the Voting API.

The Lifecycle of the Contest

Submission

The submission form we created is, at its base, a multi-step Webform. It is divided into three pages of questions and includes input for text, images, and video. The form wizard advances users from page to page, with the final submittal kicking off additional processes. A visitor can save an incomplete submission and return to it later. The Webform module contains all of these features, and it saved our team a lot of work.

Example of a multi-step webform

After pressing Submit, there is some custom pre-processing that happens. In this case, we needed to execute a database lookup for one of the submitted fields. Below is an example of code that can be added to a custom module to look up a value and save it to the recently submitted webform. This is a basic code template and won’t include sanitizing user input or all the checks you might need to do on a production-level implementation.

The code snippet and other examples from this blog post are available on Github as a Gist: gist.github.com/bookworm2000/cd9806579da354d2dd116a44bb22b04c.

use \Drupal\webform\Entity\WebformSubmission;

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function mymodule_webform_submission_presave(WebformSubmission $submission) {
 // Retrieve the user input from the webform submission.
 $submitted_data = $submission->getData();
 $zipcode = $submitted_data['zipcode'];

  // Retrieve the state name from the database custom table.
  // This is calling a service included in the mymodule.services.yml file.
  $state_lookup = \Drupal::service('mymodule.state_lookup');
  $state_name = $state_lookup->findState($zipcode);

  if ($state_name) {
    // Update the webform submission state field with the state name.
    $submitted_data['state'] = $state_name;
    $submission->setData($submitted_data);
  }
}

PHP

Content Entry

The Webform Content Creator module allows you to map fields from a submission to a specified content type’s fields. In our case, we could direct all the values submitted by the contestant to be mapped to our custom Submission content type — every time a user submitted an entry, a new node of content type Submission was created, with all the values from the webform populated.

id: dog_contest_submission_content_creator
title: 'Dog Contest Submission Content Creator'
webform: mycustomform
content_type: dog_contest_submission
field_title: 'Dog Contest - [webform_submission:values:name]'
use_encrypt: false
encryption_profile: ''
elements:
  field_email:
    type: false
    webform_field: email
    custom_check: false
    custom_value: ''
  field_dog_breed:
    type: false
    webform_field: dog_breed
    custom_check: false
    custom_value: ''
  field_dog_name:
    type: false
    webform_field: dog_name
    custom_check: false
    custom_value: ''
  field_dog_story:
    type: false
    webform_field: the_funniest_thing_my_dog_ever_did
    custom_check: false
    custom_value: ''

YAML

Voting

The main technical obstacle was that there were no voting widgets that fit the project requirements exactly:

  • The client wanted a voting field to display on each submission with a dropdown list of numbers 1 through 10
  • The default value for the field should be 1 on the first time a judge accessed the submission page
  • Any other time the judge viewed the submission, the voting field value should default to the number the judge gave (i.e. 9 out of 10)
  • The voting had to support multiple judges, such that the field value would display with the individual score given by each individual judge

The Fivestar contributed module is the module used most often on Drupal sites to implement the Voting API. It is extensible enough to allow developers or designers to add custom icons and customized CSS. The basic structure of the widget is always the same, unfortunately, and was not suitable for our contest. The available widget options are default stars, small stars, small circles, hearts, flames, or “Craft,” as shown:

The Fivestar module voting options

We enabled the Fivestar module and opted to override the base widget in order to implement a new one.

Technical Deep Dive

After enabling the Voting API Widgets module, we could now add our voting field to the submission content type. (Add a field of type “Fivestar Rating” if you want to try out the Fivestar module’s custom field.)

Note: we used the Voting API Widgets 8.x-1.0-alpha3 releaseThere is now already an alpha5 release that introduces some structural (breaking!) changes to the base entity form that conflict with our custom solution. Job security!

Select a new field of type “Voting API”

In the field settings for the new Voting API field, the only plugin choices available are plugins from the Fivestar module.

Select a vote type and plugin for the Score

We could not use those plugins for our use case, so we went ahead and built a custom plugin to extend the VotingApiWidgetBase class. For the purpose of this example, let’s call it “SpecialWidget.”

Additionally, we needed to have the ability to set defaults for the voting widget. For that, we had to add a custom form that extends the BaseRatingForm class. The example class here is called “TenRatingForm,” since the voting widget will display a 1-to-10 dropdown list for the judges.

The file structure for the directories and files in the custom module reads like this:

modules
- contrib
- custom
-- mymodule
--- src
---- Form
----- TenRatingForm.php
---- Plugin
----- votingapi_widget
------ SpecialWidget.php
---- StateLookup.php
--- mymodule.info.yml
--- mymodule.module
--- mymodule.services.yml

Let’s look at the SpecialWidget.php file in more detail. It is fairly straightforward, composed of a namespace, class declaration, and 2 inherited methods.

The namespace references the custom module. The annotation sets the widget values list, so you can adjust easily to add your own content. It is critical to include a “use” statement to incorporate the VotingApiWidgetBase class, or else the buildForm( and getStyles() methods from the parent class will not be found, and angry error messages will show up when you try to use your new custom voting widget.

namespace Drupal\mymodule\Plugin\votingapi_widget;
use Drupal\votingapi_widgets\Plugin\VotingApiWidgetBase;
    /**
     * Custom widget for voting.
     *
     * @VotingApiWidget(
     *   id = "special",
     *   label = @Translation("Special rating"),
     *   values = {
     *    1 = @Translation("1"),
     *    2 = @Translation("2"),
     *    3 = @Translation("3"),
     *    4 = @Translation("4"),
     *    5 = @Translation("5"),
     *    6 = @Translation("6"),
     *    7 = @Translation("7"),
     *    8 = @Translation("8"),
     *    9 = @Translation("9"),
     *    10 = @Translation("10"),
     *   },
     * )
     */

PHP

The other important section to point out is defining the class and the buildForm() method. The SpecialWidget class will now inherit the methods from VotingApiWidgetBase, so you do not need to copy all of them over.

class SpecialWidget extends VotingApiWidgetBase {
  /**
   * Vote form.
   */
  public function buildForm($entity_type, $entity_bundle, $entity_id, $vote_type, $field_name, $style, $show_results, $read_only = FALSE): array {
    $form = $this->getForm($entity_type, $entity_bundle, $entity_id, $vote_type, $field_name, $style, $show_results, $read_only);
    $build = [
      'rating' => [
        '#theme' => 'container',
        '#attributes' => [
          'class' => [
            'votingapi-widgets',
            'special',
            ($read_only) ? 'read_only' : '',
          ],
        ],
        '#children' => [
          'form' => $form,
        ],
      ],
    ];
    return $build;
  }
}

PHP

One additional crucial step is overriding the order of operations with which the entity builds occur. The Voting API Widgets module takes precedence over custom modules, so it is necessary to strong-arm your module to the front to be able to see changes. Ensure that the custom plugin is being called by the Voting API Widgets module, and then also ensure that the mymodule_entity_type_build() in the custom module takes precedence over the votingapi_widgets_entity_type_build() call. These functions go in the mymodule.module file.

/**
 * Implements hook_entity_type_build().
 */
function mymodule_entity_type_build(array &$entity_types) {
  $plugins = \Drupal::service('plugin.manager.voting_api_widget.processor')->getDefinitions();
  foreach ($plugins as $plugin_id => $definition) {
    // Override the votingapi_widgets form class for the custom widgets.
    if ($plugin_id === 'special') {
      $entity_types['vote']->setFormClass('votingapi_' . $plugin_id,
        'Drupal\mymodule\Form\TenRatingForm');
    }
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function mymodule_module_implements_alter(array &$implementations, string $hook) {
  if ($hook === 'entity_type_build') {
    $group = $implementations;
    $group = $implementations['mymodule'];
    unset($implementations['mymodule']);
    $implementations['mymodule'] = $group;
  }
}

PHP

After adding the custom plugin to the custom module, the option to select the widget will be available for use by the Voting API field. (After clearing all caches, of course.) The field here is called Score.

Select a vote type and plugin for the Score

Adjusting the style of the widget can be done in the “Manage Display” section for the content type:

Choose a display option for the VotingAPI Formatter in Manage Display

And here is how it looks when the voting field has been added to the Submission content type:

The last piece of the puzzle are the permissions. As with most custom features, your existing user roles and permissions will need to be configured to allow users to vote, change votes, clear votes, etc… — all the features that the Voting API Widgets module provides. Unless the voting will be done by authenticated users, most of the boxes should be checked for anonymous users — the general public.

The Permissions screen and the VotingAPI options

The judges can now vote on the node. A judge can vote as many times as they want, according to our client’s specs. Each vote will be saved by the Voting API. Depending on how you want to use the voting data, you can opt to display the most recent vote, the average of all votes, or even the sum of votes.

All voting data will be entered into the votingapi_vote table of your Drupal database by the Voting API module.

SQL Create statement for the Voting API showing how the data is stored

Success!

To Wrap it All Up

We hope you can appreciate the benefit of leveraging open source software within the Drupal ecosystem to power your projects and campaigns. Between the Voting API and Voting API Widgets modules alone, there were over 5,000 lines of code that our engineers did not have to write. Extending an existing codebase that has been designed with OOP principles in mind is a major strength of Drupal 8.

While not formally decoupled, we were able to separate the webform and submission theming structurally from the voting functionality so that our designers and engineers could work side-by-side to deliver this project. Credit to our team members Phil Frilling and Ben Holt for technical assists. The client rated us 10 out of 10 for satisfaction after voting was formally opened in a live environment!

Related tags: APIs Drupal