1

For a quiz node, I want to track how long it takes a user to submit a node form (the time from Drupal generating the form until the time that the user presses submit), and I want to store this duration in a field on my node.

Use case

Each node of content type Quiz is a 10 question quiz. I want to track the time that users spend filling out the node in order to track their improvement in speed over time.

I do NOT want to track the time spent on the website in general; for that, I already use Google Analytics with scroll tracking, and there is also the Time Spent module.

What I tried

On my node, I added an Integer field, Duration. My thought was that I could calculate the interval from the timestamp when the form was generated until the time the node was saved by adding code in hook_node_presave():

  $original_time = ??? // How do I get the time when the node form was generated? 
  $current_time = \Drupal::time()->getCurrentTime();
  $duration = $current_time - $original_time;

Where I'm stuck is I don't know how to get the time when the node form was created.

I tried to add a Datetime field to my node with a Default date of Current date, but the current date is not set until the node is saved, so that doesn't work.

So, how can I track the time from generating a node edit form until the time the user presses submit?

1
  • 2
    I would take a look at the Honeypot module which also tracks the time between showing a form and submitting it. It uses this as part of a strategy to detect spam bots. Check out honeypot_add_form_protection() and _honeypot_time_restriction_validate().
    – pfrenssen
    Commented Aug 18, 2020 at 10:03

3 Answers 3

3
+100

Here is how I would do it:

/*
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_node_quiz_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  $form['field_duration']['widget'][0]['value']['#type'] = 'hidden'; // hide field
  $form['field_duration']['widget'][0]['value']['#default_value'] = time(); // set default value to start time
  array_unshift($form['actions']['submit']['#submit'], 'mymodule_calc_duration'); // adds a custom submit that runs before the default node submit
}

function mymodule_calc_duration(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $start_time = $form_state->getValue('field_duration')[0]['value']; // get default value
  $duration = time() - $start_time; // calculate duration
  $duration_value = [
    ['value' => $duration],
  ];
  $form_state->setValue('field_duration', $duration_value); // set duration value
}
2
  • 1
    Simple and clean solution
    – Codium
    Commented Aug 18, 2020 at 18:05
  • Updated the answer, its actually better practice to put in in a submit function. Doing it in validation would still be ok if the only validation is just the "required field" aspect, which in D8 it happens on the browser via jQuery. But if there is other validation that does not occur on the browser, it would cause the time to reset. So doing it on submit is full proof.
    – No Sssweat
    Commented Aug 23, 2020 at 11:10
1

Based on NoSssweat's code, I was able to get a working solution for my site.

Note the use of \Drupal::time()->getCurrentTime() instead of time(); this is necessary if you are using something like the Datetime Testing module to change the time for behat tests, because in that case you have to rely on Drupal's time, not the server time.

/*
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_node_quiz_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  $form['field_duration']['widget'][0]['field_duration']['#type'] = 'hidden'; // hide field
  $form['field_duration']['widget'][0]['field_duration']['#default_value'] = \Drupal::time()->getCurrentTime(); // set default value to start time
  $form['#validate'][] = 'mymodule_calc_duration'; // adds custom validation that runs after all other validation passes
}

function mymodule_calc_duration(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $start_time = $form_state->getValue('field_duration'); // get default value
  $duration = \Drupal::time()->getCurrentTime() - $start_time; // calculate duration
  $duration_value = [
    ['value' => $duration],
  ];
  $form_state->setValue('field_duration', $duration_value); // set duration value
}
-1
  1. create a field in your form and in Hook_form_alter and hide it
  if ($form_id == 'form_id') {
      $form['field_view_node_start']['#access'] = FALSE;
      $form['field_view_node_start']['widget'][0]['value']['#default_value'] = time();
  }
  1. at hook_entity_presave get time between field_view_node_start and field_view_node_end
function hook_entity_presave(Drupal\Core\Entity\EntityInterface $entity) {
      $entity->set("field_view_node_end",time());
}
3
  • That doesn't work; the values of time() in hook_form_alter() and hook_entity_presave() are the same. Commented Jul 29, 2020 at 1:50
  • Is it possible, that it's not the value of time(), which is the same, but that you assign a variable in hook_form_alter(), the value of which is overridden again when you submit the form? Or did you test it exactly in the suggested way?
    – adrum99
    Commented Jul 29, 2020 at 9:23
  • 3
    Yes, the Form API does not cache the first build of a form. You could use a hidden form element which value you then find in the submitted data in a submit handler. Or use a session value to store the start time on the server if you want to make sure the browser doesn't manipulate the submitted data.
    – 4uk4
    Commented Jul 29, 2020 at 9:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.