drupal

Stop telling users that another user has modified the content in Drupal 8

Every Drupal developer knows the following error message (maybe some by heart): The content has been modified by another user, changes cannot be saved. In Drupal 8 the message is even a bit longer: The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved. While this inbuilt mechanism is very useful to preserve data integrity, the only way to get rid of the message is to reload the form and then redo the changes you want to make. This can be (or should I say 'is') very frustrating for users, especially when they have no idea why this is happening. In an environment where multiple users modify the same content, there are solutions like the Content locking module to get overcome this nagging problem. But what if your content changes a lot by backend calls ?

On a big project I'm currently working on, Musescore.com (D6 to D8), members can upload their scores to the website. On save, the file is send to Amazon where it will be processed so you can play and listen to the music in your browser. Depending on the length of a score, the processing might take a couple of minutes before it's available. In the meantime, you can edit the score because the user might want to update the title, body content, or add some new tags. While the edit form is open, the backend might be pinging back to our application notifying the score is now ready for playing and will update field values, thus saving the node. At this very moment, the changed time has been updated to the future, so when the user wants to save new values, Drupal will complain. This is just a simple example, in reality, the backend workers might be pinging a couple of times back on several occasions doing various operations and updating field values. And ironically, the user doesn't even have any permission to update one or more of these properties on the form itself. If you have ever uploaded a video to YouTube, you know that while your video is processing you can happily update your content and tags without any problem at all. That's what we want here too.

In Drupal 8, validating an entity is now decoupled from form validation. More information can be found on the Entity Validation API handbook and how they integrate with Symfony. Now, the validation plugin responsible for that message lives in EntityChangedConstraint and EntityChangedConstraintValidator. Since they are plugins, we can easily swap out the class and depending on our needs only add the violation when we really want to. What we also want is to preserve values of fields that might have been updated by a previous operation, in our case a backend call pinging back to tell us that the score is now ready for playing. Are you ready ? Here goes!

Step 1. Swap the class

All plugin managers in Core (any plugin manager should do that!) allow you to alter the definitions, so let's change the class to our own custom class.

<?php
/**
* Implements hook_validation_constraint_alter().
*/
function project_validation_constraint_alter(array &$definitions) {
  if (isset(
$definitions['EntityChanged'])) {
   
$definitions['EntityChanged']['class'] = 'Drupal\project\Plugin\Validation\Constraint\CustomEntityChangedConstraint';
  }
}
?>

For the actual class itself, you can copy the original one, but without the annotation. The constraint plugin manager doesn't need to know about an additional new one (unless you want it to of course).

<?php
namespace Drupal\project\Plugin\Validation\Constraint;

use
Symfony\Component\Validator\Constraint;

/**
* Custom implementation of the validation constraint for the entity changed timestamp.
*/
class CustomEntityChangedConstraint extends Constraint {
  public
$message = 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved. In case you still see this, then you are really unlucky this time!';
}
?>

Step 2: alter the node form

We want to be able to know that a validation of an entity is happening when an actual form is submitted. For this, we're adding a hidden field which stores a token based on the node id which we can then use later.

<?php
/**
* Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm.
*/
function project_form_node_form_alter(&$form, &$form_state) {
 
/** @var \Drupal\Node\NodeInterface $node */
 
$node = $form_state->getFormObject()->getEntity();
  if (!
$node->isNew() && $node->bundle() == 'your_bundle' && $node->getOwnerId() == \Drupal::currentUser()->id()) {
   
$form['web_submission'] = [
     
'#type' => 'hidden',
     
'#value' => \Drupal::csrfToken()->get($node->id()),
    ];
  }
}
?>

Step 3: Validating the entity and storing an id for later

We're getting to the tricky part. Not adding a violation is easy, but the entity that comes inside the constraint can't be changed. The reason is that ContentEntityForm rebuilts the entity when it comes in the submission phase, which means that if you would make any changes to the entity during validation, they would be lost. And it's a good idea anyway as other constraints might add violations which are necessary. To come around that, our constraint, in case the changed time is in the past, will verify if there is a valid token and call a function to store the id of the node in a static variable which can be picked up later.

<?php
namespace Drupal\project\Plugin\Validation\Constraint;

use
Symfony\Component\Validator\Constraint;
use
Symfony\Component\Validator\ConstraintValidator;

/**
* Validates the EntityChanged constraint.
*/
class CustomEntityChangedConstraintValidator extends ConstraintValidator {

 
/**
   * {@inheritdoc}
   */
 
public function validate($entity, Constraint $constraint) {
    if (isset(
$entity)) {
     
/** @var \Drupal\Core\Entity\EntityInterface $entity */
     
if (!$entity->isNew()) {
       
$saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
       
// A change to any other translation must add a violation to the current
        // translation because there might be untranslatable shared fields.
       
if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
         
$add_violation = TRUE;
          if (
$entity->getEntityTypeId() == 'node' && $entity->bundle() == 'your_bundle' &&
           
$this->isValidWebsubmission($entity->id())) {
           
$add_violation = FALSE;

           
// Store this id.
           
project_preserve_values_from_original_entity($entity->id(), TRUE);
          }

         
// Add the violation if necessary.
         
if ($add_violation) {
           
$this->context->addViolation($constraint->message);
          }
        }
      }
    }
  }

 
/**
   * Validate the web submission.
   *
   * @param $value
   *   The value.
   *
   * @see project_form_node_form_alter().
   *
   * @return bool
   */
 
public function isValidWebsubmission($value) {
    if (!empty(\
Drupal::request()->get('web_submission'))) {
      return \
Drupal::csrfToken()->validate(\Drupal::request()->get('web_submission'), $value);
    }

    return
FALSE;
  }

}

/**
* Function which holds a static array with ids of entities which need to
* preserve values from the original entity.
*
* @param $id
*   The entity id.
* @param bool $set
*   Whether to store the id or not.
*
* @return bool
*   TRUE if id is set in the $ids array or not.
*/
function project_preserve_values_from_original_entity($id, $set = FALSE) {
  static
$ids = [];

  if (
$set && !isset($ids[$id])) {
   
$ids[$id] = TRUE;
  }

  return isset(
$ids[$id]) ? TRUE : FALSE;
}
?>

Step 4: copy over values from the original entity

So we now passed validation, even if the submitted changed timestamp is in the past of the last saved version of this node. Now we need to copy over values that might have been changed by another process that we want to preserve. In hook_node_presave() we can call project_preserve_values_from_original_entity() to ask if this entity is eligible for this operation. If so, we can just do our thing and happily copy those values, while keeping the fields that the user has changed in tact.

<?php
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function project_node_presave(NodeInterface $node) {
  if (!
$node->isNew() && isset($node->original) && $node->bundle() == 'your_bundle' && project_preserve_values_from_original_entity($node->id())) {
   
$node->set('your_field', $node->original->get('your_field')->value);
   
// Do many more copies here.
 
}
}
?>

A happy user!

Not only the user is happy: backends can update whenever they want and customer support does not have to explain anymore where this annoying user facing message is coming from.

Topics 

drupal, planet, usability

Taking a (Drupal 8) website offline using AppCache

A native mobile application which can cache the data locally is a way to make content available offline. However, not everyone has the time and/or money to create a dedicated app, and frankly, it's not always an additional asset. What if browsers could work without network connection but still serve content: Application Cache and/or Service Workers to the rescue!

For Frontend United 2016, Mathieu and I experimented to see how far we could take AppCache and make the sessions, speakers and some additional content available offline using data from within the Drupal site. There are a couple of pitfalls when implementing this, of which some are nasty (see the list apart link at the bottom for more information). Comes in Drupal which adds another layer of complexity, with its dynamic nature of content and themes. Javascript and css aggregation is also extremely tricky to get right. So after trial and error and a lot of reading, we came up with the following concept:

  1. Only add the manifest attribute to all "offline" pages which are completely separate from "online pages", even though they might serve the same content. In other words, you create a sandboxed version of some content of your site which can live on its own. Another technique is a hidden iframe which loads a page which contains the html tag with the manifest attribute. You can embed this iframe on any page you like. This gives you the option to create a page where you link to as an opt-in to get a site offline. Both techniques give us full control and no side affects so that when network is available the site works normally.
  2. You define the pages which you want to store in the cache. They are served by Drupal, but on a different route than the original (e.g. node/1 becomes offline/node/1) and use different templates. These are twig templates so you can override the defaults to your own needs. Other information like stylesheet and javascript files can be configured too to be included.
  3. The manifest thus contains everything that we need to create the offline version when your device has no network connection. In our case, it contains the list of speakers and sessions, content pages and some assets like javascript, stylesheet, logo and images.

Offline in the browser or on the homescreen

Go to the Offline homepage of Frontend United and wait until the 'The content is now available offline!' message appears, which means you just downloaded 672 kb of data - it is really really small, surprising no? Now switch off your network connection and reload the browser: still there! Click around and you'll be able to check the offline version at any time. If you're on a mobile device, the experience can be even sweeter: you can add this page to your homescreen, making it available as an 'app'. On iOS, you need to open the app once while still being connected to the network. We really do hope safari/iOS fixes this behavior since this is not necessary on Android. After that, turn off your network and launch the app again. Oh, and it works on a watch too if you have a browser on it. If that isn't cool, we don't know what else is! We have a little video to show you how it looks like. Watch (pun intended) and enjoy! Oh, in case we make changes to the pages, you will see a different notification telling you that the content has been updated - if your device has network of course.

Drupal integration

We've created a new project on Drupal.org, called Offline App, available for Drupal 8. The project contains the necessary code and routes for generating the appcache, iframe, pages (nodes and views) and settings to manipulate the manifest content. 3 new regions are exposed in which you can place the content for offline use. Those regions are used in offline-app-page.html.twig - but any region is available if you want to customize. Two additional view modes are created for content types and the read more link can be made available in the 'Offline teaser' mode. Formatters are available for long texts to strip internal links and certain tags (e.g. embed and iframe) and for images that will make sure that 'Link to content' is pointing to the 'Offline path'. Last, but not least, an 'Offline' Views display is available for creating lists. We're still in the process in making everything even more flexible and less error-prone when configuring the application. However, the code that is currently available, is used as is on the Fronted United website right now.

This module does not pretend to be the ultimate solution for offline content, see it as an example to quickly expose a manifest containing URL's from an existing Drupal installation for an offline version of your website. Other Drupal projects are available trying to integrate with AppCache or Service workers, however, some are unsupported or in a very premature state, apart from https://www.drupal.org/project/pwa. Note that I've been in contact with Théodore already and we'll see how we combine our efforts for coming up with one single solution instead of having multiple ones.

What about service workers ?

Not all browsers support the API yet. Even though AppCache is marked deprecated, we wanted to make sure everyone could have the same offline experience. However, we'll start adding support for service workers soon using the same concept.

We're also planning to start experimenting with delivering personal content as well, since that's also possible, yet a little trickier.

Links

Note: closed the comments for now as anti-spam modules are not doing their job nicely

Drupal 8 logo Pepper's ghost illusion

Turning your smartphone into a hologram projector has become fairly popular nowadays, although it's technically called Pepper's ghost to create the illusion. Anyway, I had to try it myself too, right ? Building the pyramid isn't that hard and the first try-out is pretty cool to be honest.

So with that going fine, I opened up Blender to create my own animation just for fun. I took the Drupal 8 logo, ripped it apart and let it build itself up again. I quickly wrote an Android application that would just play 4 videos on a surface; and each video in a different rotation. The app is attached at the end if you want to try it out yourself (works well on Motorala, with Lollipop). If all goes well, I'll make it possible to select your own video and make the code public on GitHub. But for now, just enjoy the video. Or start building your own pyramid, it's really fun.

I've also created one with the Druplicon, based on on a model made by @berkes

eps & kaas

2014 marks the beginning of a new company called eps & kaas which I cofounded with 3 other people. We're a digital agency, providing services ranging from mobile applications, websites, consultancy and design.

While you can still contact us (and me, from time to time) for Drupal development, my personal focus is now on Android and iOS development, but also different web technologies like Symfony or Ruby on Rails.

You can find us on the web, Twitter and Facebook. Drop us a note if you think we can help you!

So what about contributions ?

No, I'm not leaving Drupal. However, it's not going to the main technology anymore in my day-to-day work which is why I'm minimising my work on contributed modules: Display Suite is now in the hands of Bram 'aspilicious' Goffings, who has been a major contributor for the module since years. Most other projects have found a new maintainer, others are still waiting, maybe you're interested ?

I'm still around co-maintaining Field API for Drupal 8, or helping out in other areas that interest me, most notable the new configuration management system. We're slowly seeing the end of - yet another - long development cycle, hopefully marked with a release in june. The next big effort to get as many as bright minds together in one place is happening in March for the Drupal Developer Days in Szeged, Hungary. Anyone can help out in different ways: programming, reviewing, playing music, sponsoring plane/hotel tickets and so on.

Music and play

I've been writing songs since september for an album I want to get out this year - no release date yet. This is currently on hold as I need to rehearse songs to play with Last Exit To Loonville on the preselection of Humo's Rock Rally on the 25th of january in Gent. Tickets and more details are on the site of the Vooruit where the event is taking place. So, 8 years after my first appearance on Belgium's biggest rock talent event and more or less the last time I've been on a big stage, I'm back. What a great feeling that will be.

And, as if this year isn't going to be busy enough, I've started writing a play. The goal is to get it performed somewhere in october, so there's still some six months to finish the script. The main characters are alive in my head, the plot is as good as ready; if you're into a bit of absurdness and feel like Mark Everett and the theories of his dad are interesting topics, you'll want to attend this!

I'm sure 2014 is going to be a fantastic year, let it be a great one for you too!

An open source app for DrupalCamps

On september 14 and 15, Leuven will host the annual Belgium DrupalCamp. During those two days, people come together learning and discussing the open-source content management system Drupal. The program will be available on the website, but we decided to also create an application this year. We've tried to make it abstract as possible, so other Drupal events can easily built from the source code which is available online.

The apps will be available for Android and iOS. As soon as the program is ready, we'll publish them, so keep an eye out for the camp website, twitter or, of course, this article. The Android version is available on Play Store.

Features include:

  • Built with speed in mind: native app
  • Works offline
  • Sessions and speakers
  • Mark your favorite sessions
  • Maps integration when online

Collaborate

The code is freely available on GitHub, so anyone can easily send in bug reports, interface translations or create pull requests to make the applications even better.

Proudly built by @swentel, @TimLeytens and @leenvs.

So, who will make the Windows mobile and Firefox version ?

Pages

Subscribe to RSS - drupal

You are here