The feature request is to return to the last submitted step within a multistep form in case the user's internet connection / computer dies, etc. It would be a cool feature for the project, and the quick hack that I implemented without knowing the in's or out's of the multi-step internals.

function webform_client_form(...) {
    // Put the components into a tree structure.
    if (!isset($form_state['storage']['component_tree'])) {
      $form_state['webform']['component_tree'] = array();
      $form_state['webform']['page_count'] = 1;
      $form_state['webform']['page_num'] = 1;
      _webform_components_tree_build($node->webform['components'], $form_state['webform']['component_tree'], 0, $form_state['webform']['page_count']);

      ### New code below ####
      # @todo
      # - make configurable
      # - ensure that there is another page, 
      #   i.e. I think a single page form with save draft would break using the below code.
      # - only activate if same user?
      # - Check that it works with a normal single / multipage webform ;)

      // If draft return to the last submitted page.
      if ($is_draft && isset($submission->data)) {
        $page_nums = array(1);
        foreach ($submission->data as $cid => $values) {
          if (isset($node->webform['components'][$cid]) && isset($node->webform['components'][$cid]['page_num'])) {
            $page_nums[] = $node->webform['components'][$cid]['page_num'];
          }
        }
        $last_page = max($page_nums);
        if ($last_page < $form_state['webform']['page_count']) {
          $form_state['webform']['page_num'] = $last_page + 1;
        }
      }

      // If preview is enabled, increase the page count by one.
      if ($node->webform['preview']) {
        $form_state['webform']['page_count']++;
      }
    }
    else {
    ...
    }

Our particular use-case is very specific: We are doing a webform for a company that does not want their clients to view previous pages in a 35 page survey but they want their clients to be able to return to the last submitted page should their computer crash, etc. It has something to do with sociological profiling where they should not be able to reference their previously answered responses, and yes, print screen etc as been covered!

The above hack, hiding the previous button and the following updated HTTP header seems to lock the user nicely into a forward directional process with the previous pages completely hidden:

Cache-Control: no-cache, no-store, must-revalidate, post-check=0, pre-check=0

The no-store is the newly added component to the string.

We will be adding a JScript history clear too, but with the above mods, the user simply gets served with the last page, even if they hit the browsers back button. :)

Comments

mostovoy’s picture

Hi:

We have the same requirement.

Could you solve it?

Thanks Regards

danchadwick’s picture

Status: Active » Closed (won't fix)

I don't see this being incorporated into webform.

alan d.’s picture

Status: Closed (won't fix) » Active

Thanks for the feedback, how about a simple alter for the latest current page so this is possible without hacking the module?

Over the years, we have done maybe 8 webforms that are 10+ pages and to be able to go to the final draft page in the process would be benefital for all forms, irrespective of the whacky nature of this particular use case.

danchadwick’s picture

@Alan D. -- Is there no way to achieve this with a form_alter or process function? I certainly don't want to force people to hack webform to achieve this.

alan d.’s picture

This governs the actual construction of the form way way before a form alter could kick into life. No test environment for helping much, but since this is set:

  $form['#node'] = $node;
  $form['#submission'] = $submission;
  $form['#is_draft'] = $is_draft;

I think that this is all that would be needed:

    // Put the components into a tree structure.
    if (!isset($form_state['storage']['component_tree'])) {
      $form_state['webform']['component_tree'] = array();
      $form_state['webform']['page_count'] = 1;
      $form_state['webform']['page_num'] = 1;
      _webform_components_tree_build($node->webform['components'], $form_state['webform']['component_tree'], 0, $form_state['webform']['page_count']);
      drupal_alter('webform_client_form_initation', $form, $form_state);
    }

"webform_client_form_initation" For lack of a better name...

An untested example of the above could be:

/**
 * Provides a way to alter the webform behaviour before it is constructed.
 */
function hook_webform_client_form_initation_alter(&$form, &$form_state) {
  $node = $form['#node'];
  $submission = $form['#submission'];
  $is_draft = $form['#is_draft'];
    if ($is_draft && isset($submission->data)) {
      $page_nums = array(1);
      foreach ($submission->data as $cid => $values) {
        if (isset($node->webform['components'][$cid]) && isset($node->webform['components'][$cid]['page_num'])) {
          $page_nums[] = $node->webform['components'][$cid]['page_num'];
        }
      }
      $last_page = max($page_nums);
      if ($last_page < $form_state['webform']['page_count']) {
        $form_state['webform']['page_num'] = $last_page + 1;
      }
    }
    // If preview is enabled, increase the page count by one.
    if ($node->webform['preview']) {
      $form_state['webform']['page_count']++;
    }
}
alan d.’s picture

StatusFileSize
new1.86 KB

To clarify, the above would lead to the final draft page that the user was on, but nothing else.

Our use case would then use a form alter to block the previous button / browser button:

Ensure the right headers

/**
 * Implements hook_boot().
 */
function jm_boot() {
  if (!drupal_is_cli()) {
    drupal_add_http_header('Cache-Control', 'no-cache, no-store, must-revalidate, post-check=0, pre-check=0');
    drupal_add_http_header('Pragma', 'no-cache');
  }
}

Hide the back button & add the JScript trigger (one way of 4 or 5 that I tested)

/**
 * Implements hook_form_FORM_alter() for user_profile_form().
 */
function jm_form_webform_client_form_alter(&$form, $form_state) {
  if (isset($form['actions']['previous'])) {
    $form['actions']['previous']['#access'] = FALSE;
  }
  $form['#action'] .= '#no-back';
}

And the JScript used is attached. I just defined this in the modules info file. (Sick of form validation errors bypassing the form alters... a known core Drupal bug that is "By design")

scripts[] = js/disable-history.js

Note: disable-history.jscript.txt rename to disable-history.js

danchadwick’s picture

Found this previous issue: #1627936: Resume webform from last submitted question when saved as draft

3 years ago in that issue, quicksketch said that it was difficult to resume a draft on the last page that the user was on. The primary reason was caching. But we are currently showing messages on this page when a draft is resumed and currently we don't support anonymous drafts. So we're talking about an authenticated use on a page with a drupal_set_message, which makes the page uncacheable.

I'm working on a patch that remembers in the database the highest validated draft page that the user completed for this submission. If the submission is a draft, they are sent to the next available (i.e. not-conditionally-hidden) page.

If they resume via the view node page, then additionally a message is presented: "A partially-completed draft of this form was found." Editing the submission from the submission page's EDIT tab doesn't present this message since they were just looking at the draft. But it does "fast forward" to the next appropriate page.

At this point, I'm not thinking that this behavior will be optional. But if the current behavior is desired, some settings.php, webform global config or (gag) node-specific option could be introduced.

danchadwick’s picture

Here's a patch to play with. I know of at least one bug (if you resume and end up on the preview page, you get PHP notices and probably a loss of data if you submit).

You'll need to run update.php or drush updatedb. If you remove the patch (and this patch doesn't get committed), you'll either have to restore your database or manually decrement the webform schema number in the system table and delete the valid_page column from the webform_submissions table.

I'd like to get some feedback before going forward.

quicksketch’s picture

Hi Dan, this looks like a great patch to me. The implementation is pretty trivial, and you're right that caching is not a concern with our current draft functionality. When we try supporting drafts for anonymous users, this may add some extra work but I doubt it would be much.

I also agree that this should just be always-enabled. Resuming where you left off is much, much more useful than the beginning of the form.

From a learn-ability stand-point, I think the only thing I'd recommend is that we use a more verbose property and database column name than "valid_page", which sounds like a boolean flag rather than a page number. If this is only used for drafts, perhaps "draft_page_number"?

Everything else is just code review:

+      'nid_is_draft' => array('nid', 'is_draft'),

This new index is added in hook_schema() but needs to be added in the webform_update_7429() as well.

+/**
+ * Add a column to the submission table to store the page on which to resume a draft. Sites with many, many submissions may wish to execute this update with 'drush updatedb'.
+ */

Just coding-standards, this code comment should wrap at 80 characters. Though even more picky, the first line should be stand alone. So the proper formatting would be:

/**
 * Add a column to the submission table to store the page on which to resume a draft.
 *
 * Sites with many, many submissions may wish to execute this update with 'drush updatedb'.
 */
+        drupal_set_message(t('A partially-completed draft of this form was found.'));

I think displaying a message is great. Could we use a little less specific terms, as "draft" may not be a term used anywhere else in the UI if we're autosaving. Does this sound any better? "A partially-completed form was found. Please complete the remaining portions."

+        // Force a preview to avert an unintended submission via Next.
+        $form_state['webform']['preview'] = TRUE;

I'm not sure I understand this line (or the code comment). This isn't going to force a preview if the preview functionality isn't even enabled in the node configuration will it? Or is this just resuming a submission that got all the way to prevent but didn't actually hit the final submit button?

+  // or the previious page (if not) as the last valid page.

Minor typo in "previious"

+  $submission->valid_page = $is_draft
+                              ? (end($form_state['clicked_button']['#parents']) == 'next' && $form_state['values']['op'] != '__AUTOSAVE__'
+                                  ? $form_state['webform']['page_num']
+                                  : $form_state['webform']['page_num'] - 1
+                                )
+                              : 0;

Could we just separate this for readability into an IF/ELSE instead of nested ternary operators?

if ($is_draft) {
  $submission->valid_page = end($form_state['clicked_button']['#parents']) == 'next' && $form_state['values']['op'] != '__AUTOSAVE__';
}
else {
  $submission->valid_page = $page_num ? $page_num - 1 : 0;
}

Looks great overall!

danchadwick’s picture

Thanks for the review, Nate:

1) Yikes. That index was left over from functionality I ripped back out. I *was* going to delete all the "valid_page" entries for drafts when any component nor conditional changed, but then decided against it. Webform is full of issues if you change the node's meta-schema after there are already entries. For example, existing forms can easily become invalid, so why treat drafts any differently? I would have caught this one when I reviewed the changes prior to commit, luckily.

2) There is an exception to the docblock rule for hook_update_N functions because they form the prompt for update.php, both in term of the tense and length of the comment.

3) I like your message better. What I had was my second or third try. Thanks.

4) That Preview-forcing line handles the case where otherwise the form would be submitted. We insert a preview page if otherwise the last string of pages in the form are all hidden. This avoid an unintended submission. This line of code duplicate what happens when you click Next Page.

5) I'm gonna disagree on the nested tertiary operator. I find it clearer. When I read that, with the indenting I use, I immediately see "this variable is always set here and this expression calculates the right value". When split into an if else over 6 lines of code, I have to work harder to see that.

fenstrat’s picture

Just a quick comment on nested ternaries: I agree with Nate. if/else is more readable and in line with Drupal coding standards.

danchadwick’s picture

Status: Active » Fixed
StatusFileSize
new9.77 KB

I named the field "highest_valid_page" which is clearly not a boolean and indicates what it is. "draft_page_number" implies that if the user clicked "Save draft" on page 4, that it would be page 4. It wouldn't. It would be page 3, since page 4 isn't validated yet.

I was unconvinced about the ternary operator until I realized that Nate's re-write of the expression was completely wrong. If Nate can mis-read it, then anyone can. So I removed the nested ternary. I only puked a little as I clicked "Save".

This patch also fixes an incorrect, unrelated copy-and-past comment error, and renames a misleading argument to webform_client_form. "$is_draft" doesn't mean that this form should be a draft. That status only comes from the submission itself. It means "Hey I found a draft from the node's view page". Accordingly, the formal argument is new "$resume_draft". No functional difference.

The (unused in webform) $form['#is_draft'] expression was changed to reflect what was intended: $submission->is_draft.

Committed to 7.x-4.x.

danchadwick’s picture

Version: 7.x-4.x-dev » 8.x-4.x-dev
Category: Feature request » Task
Status: Fixed » Patch (to be ported)

Needs port to 8.x.

  • DanChadwick committed b80b3cb on 7.x-4.x
    Issue #2278177 by DanChadwick: Return to the last page when restarting a...
fenstrat’s picture

Version: 8.x-4.x-dev » 7.x-4.x-dev
Category: Task » Feature request
Status: Patch (to be ported) » Fixed

Committed and pushed to 8.x-4.x. Thanks!

+++ b/webform.module
@@ -2411,7 +2412,7 @@ function webform_client_form($form, &$form_state, $node, $submission = FALSE, $i
   // Bind arguments to $form to make them available in theming and form_alter.
   $form['#node'] = $node;
   $form['#submission'] = $submission;
-  $form['#is_draft'] = $submission ? $submission->is_draft : $is_draft;
+  $form['#is_draft'] = $submission && $submission->is_draft;
   $form['#filter'] = $filter;
 
   // Add a theme function for this form.

Current 8.x-4.x doesn't have $form['#is_draft'] = $submission ? $submission->is_draft : $is_draft; which is one of the reasons #14 didn't apply to it. Odd. I did a git blame to find out why it didn't have it which lead me to #2284595: Save as draft when form fails validation however it appears to have been correctly applied to 8.x-4.x there. Somewhere later it was deleted? Not sure, I've manually fixed it up, bloody hope this doesn't indicate a patch that wasn't applied to 8.x-4.x which comes back to bite later.

  • fenstrat committed 9bc9628 on 8.x-4.x authored by DanChadwick
    Issue #2278177 by DanChadwick: Return to the last page when restarting a...
danchadwick’s picture

There was an issue for that. I deleted $form['#is_draft'] from 8.x since it was never used in 7.x and that value is available from $form_state. The only reason we're keeping it in D7 is in case some module is relying on it. Cruft.

quicksketch’s picture

Yay, nice job @DanChadwick!

fenstrat’s picture

Committed and pushed follow up to fix #17.

Right of course, thanks for the heads up Dan. That issue is #2296563: $form['#is_draft'] is not used, I've just added a CR for it too https://www.drupal.org/node/2479443

  • fenstrat committed 939e16d on 8.x-4.x
    Issue #2278177 by fenstrat: Follow up to re-remove $form['#is_draft'].
    

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.