This is split off from #1966876: Allow other modules to alter the list of nodes to be published or unpublished.

It would be nice if other modules could prevent scheduled nodes from being published or unpublished.

Example use cases:

  • Articles that are submitted by users should only be published if they are approved by an administrator, ref #1955938: Publish only "approved" nodes .
  • The scheduled announcement of a new working group should not be published unless 5 people have joined the group.

Comments

pfrenssen’s picture

Status: Active » Needs review
StatusFileSize
new3.7 KB

Here is a reworked version that has been split off from the patch in #1966876: Allow other modules to alter the list of nodes to be published or unpublished. I incorporated the remarks from jonathan1055 in comment #17 of that issue, and renamed the function to scheduler_is_allowed() and the hook to hook_scheduler_allow() to avoid confusion between "allowing" and "scheduling".

pfrenssen’s picture

Here is an example implementation of this hook. Only allow a node to be scheduled if it has been approved by a moderator.

/**
 * Implements hook_scheduler_allow().
 */
function scheduler_workbench_scheduler_allow($node, $action) {
  // Only publish/unpublish nodes that are in the allowed moderation state.
  $allowed_state = variable_get('scheduler_publish_allow_state_' . $node->type, '');
  if ($allowed_state && $node->workbench_moderation['current']->state != $allowed_state) {
    return FALSE;
  }
  return TRUE;
}

This is from #1955938: Publish only "approved" nodes

pfrenssen’s picture

Fixed a bug in the patch:

-  $result = !empty($node->key);
+  $result = !empty($node->scheduler[$key]);
jonathan1055’s picture

Hi,
I think you need to be clear exactly what the intention of this patch is. Originally in #14 of #1966876: Allow other modules to alter the list of nodes to be published or unpublished you just wanted to prevent the message 'This post is unpublished and will be published on date' because on sites which implement hook_scheduler_nid_list_alter() the content may be prevented from being published via scheduler. That was a good idea. But the changes you made in the patch in #16 on that issue actually prevent a node from being scheduled in the first place.

However, in the patch in #3 above, in scheduler_node_presave() the new function scheduler_is_allowed() is used to suppress the message but it does not alter whether the node can be scheduled or not.

Can you tell what the intended functionality is, then we can discuss how to achieve it.

Jonathan

rickmanelius’s picture

I'm a bit apprehensive about committing this patch. I feel like this use case would better be served by a combination of workbench moderation + revision scheduler (http://drupal.org/project/revision_scheduler). This way you can use the workbench logic to get the node to a stage where it's ready to publish, and THEN you can schedule that revision to be published.

pfrenssen’s picture

What I want to achieve is that modules can prevent nodes from being published or unpublished. I first tried the approach in #1966876: Allow other modules to alter the list of nodes to be published or unpublished, by naively altering the list of nodes that are processed in _scheduler_publish() and _scheduler_unpublish(). However this was not sufficient, since scheduler_node_presave() also needs to know whether it should act on the node or not.

The approach in #1966876: Allow other modules to alter the list of nodes to be published or unpublished only half meets the goal. It works well for reordering the node list, but since scheduler_node_presave() still blindly acts on the node unexpected things may happen.

At the moment the only unexpected thing is that an error message appears, but functionality might be added in the future that also needs to check if publishing of a particular node is allowed. One example is #1819074: Allow publish_on dates in the past. This patch allows nodes that have a scheduling date in the past to be published immediately. To make that work in combination with this issue it would need another call to scheduler_is_allowed($node, 'publish').

pfrenssen’s picture

@jonathan1055, this patch does work in preventing the publishing and unpublishing, it is handled by these lines:

@@ -756,6 +789,13 @@ function _scheduler_publish() {
   foreach ($nids as $nid) {
     $n                 = node_load($nid);
+
+    // Allow other modules to prevent this node from being published.
+    if (!scheduler_is_allowed($n, 'publish')) {
+      continue;
+    }

I have tests covering this in #1955938: Publish only "approved" nodes and in the distribution I'm working on: scheduling.test.

If you want I can add tests for this to Scheduler as well, it would require a small test module to be added that implements the hook.

medieval111’s picture

This works perfectly for me, in combination with #1955938: Publish only "approved" nodes .
Now "role1" can create a node and set the publish date. Role2 can approve the node, so the publish date will be honored. Otherwise it won't.

Can this please be committed?

pfrenssen’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

This needs tests in the Scheduler module itself before it can be committed.

pfrenssen’s picture

Assigned: Unassigned » pfrenssen
StatusFileSize
new2.64 KB

Started working on tests. Here is my work in progress. I created a test module that provides a node type with a field that needs to be checked by the CEO before a scheduled node may be published.

pfrenssen’s picture

Worked some more on the test.

pfrenssen’s picture

Issue summary: View changes

Spelling

jonathan1055’s picture

Hi Pieter,
I still do not fully understand the purpose of these changes - see my comment in #4. Specifically my concerns are:

  1. The change in scheduler_node_presave() only prevents the message. The node is still scheduled for publishing. If the node is scheduled then we should display the message, otherwise there is a mismatch between what is done and what the user is led to believe has happened. If you want to add additional warning messages you can use a secondary validate function added to $form['#validate'][] in form_alter. For example to say 'This node is scheduled but will not get published unless it is passed by an authorised user' or whatever
  2. Additionally your $form['#validate'] could be used to prevent the node from being published immediately (if that config option is set and the user enters a past date) by checking whatever criteria you have and issueing a form_set_error()
  3. The changes in _scheduler_publish() have the effect of skipping the scheduled node if it fails hook_scheduler_allow(). Why is this different from implementing hook_scheduler_nid_list_alter() and having that function remove $nid from the list?

So overall, the functionality in this patch can all be achieved by implementing existing hooks and form #validate functions. Atleast, that is how it appears to me. I'm happy to have a discussion about it, but as it stands I cannot pass this patch for committing.

Jonathan

pfrenssen’s picture

The intention is not to suppress the error message, but to prevent nodes from being published immediately. The confusion is probably because the patch was written before this functionality was added (in issue #1819074: Allow publish_on dates in the past), but it was already being worked on at the time. See my motivation in comment #6. In the meanwhile #1819074: Allow publish_on dates in the past got in but the patch still needs to be updated so it actually prevents the publication of the nodes.

I'm starting to think that the current approach taken in this issue might not be enough. We really need a hook firing during scheduler_node_presave() so we can have control over nodes that are being published and unpublished at this point in the cycle, but we might need to add an additional parameter indicating which part of the code is firing the hook, so we can differentiate between "regular" publication on cron, and "immediate" publication on node presave.

One potential use case is #1182450: i18n support - Synchronize scheduled dates in translated nodes. In this issue problems arise because the i18n_sync module is resaving nodes, firing hook_node_presave(), causing nodes to be published prematurely. The hook_scheduler_allow() that is being added in this issue should be able to solve this, but then we should be able to distinguish between this hook firing on cron or on presave. It should only prevent publishing on presave, not on cron.

pfrenssen’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new9.31 KB
new1.87 KB

Here's an updated patch, this should make it clearer. It now actively prevents the publication of nodes in scheduler_node_presave().

BTW I'm open for suggestions on how to solve this differently.

@Jonathan, I forgot to answer on your suggestion to use form validate handlers to prevent publication. This doesn't work because this will prevent the form from being submitted, which is undesirable if you have one user role that is responsible for scheduling, and another for publication approval. Also the form validate will only fire if the node form itself is used to create the nodes. This is not always the case, some sites use custom modules for creating nodes (eg admin dashboards, data importers, ...).

Setting this to "Needs review" to see if it does not break anything. This still needs work.

jonathan1055’s picture

OK, I see your point that using an extra validate handler may not cover all cases, so a specific hook in scheduler_node_presave is required.

I've followed through your logic changes in presave, and if scheduler_is_allowed() returns false, that will prevent the immediate publishing if that option was on. But then the node gets scheduled anyway. It will then be down to the call to scheduler_is_allowed() in _scheduler_publish() to allow or still prevent publishing. This is ok, but I think you need to add a bit more of this explanation into the comment in presave, as it appeared odd when I first read what you were doing.

Some other points to consider:

  1. I'm not sure that hook_scheduler_allow() is the best name for other modules to implement. It's not clear whether we are allowing scheduling or allowing publishing/unpublishing. Something like hook_scheduler_activity_is_allowed() then the action 'publish' or 'unpublish' is the activity.
  2. Once we fix on the the external hook name, we can look again the internal named function. It might be _scheduler_activity_is_allowed()
  3. We already have hook_scheduler_nid_list() and hook_scheduler_nid_list_alter(). We will need to explain the difference and uses of the now three hook functions, so that module developers understand. Does this kind of documentation go in module onscreen help, or in a readme file?
  4. In _scheduler_unpublish() on line 1029 you can use $action not the string 'publish'
  5. Likewise in _scheduler_unpublish()
  6. Coder review flags one point - would be nice to keep this output clean:
    Line 648: The correct use of the string is 'e.g.,' (with a comma after it). However, for clarity, consider changing 'e.g.' to 'for example,'
    // converting it into the node property (e.g. 'publish_on').

Good work on the new test folder structure, thanks for starting that.

Jonathan

jonathan1055’s picture

Now we need to do a second operation to change the status.

pfrenssen’s picture

The node not being published but remaining in a scheduled state even though its publication date may already have passed is by design. This is to cover the use case of having one "editor" user role that is responsible for writing and scheduling content, and a "chief editor" user role that is responsible for proofreading and approving content. They might not be able to approve the content in time. If the date has passed, they can then either approve the content and have it published immediately, or set a new scheduling date.

  1. I'm not sure that hook_scheduler_allow() is the best name for other modules to implement. It's not clear whether we are allowing scheduling or allowing publishing/unpublishing.

    Me neither, I have broken my head several times already over the name of this hook. Now that I am implementing it for the test it bothers me again. Looking at how constraints are handled in D8 it seems to me that this concept fits better for this hook. Instead of the hook returning TRUE or FALSE it could return an array of violations (these could be strings containing error messages). If the array is empty there were no violations, and the (un)publication is allowed. What do you think of this? I like the last code example on Symfony's constraints documentation. We could adapt it to something like:

      $error_list = module_invoke_all('scheduler_action_is_allowed', 'publish');
      if (!empty($error_list)) {
        // Publishing is not allowed, display error messages in the list.
      }
      else {
        // Publish the node
      }
    
  2. Once we fix on the the external hook name, we can look again the internal named function. It might be _scheduler_activity_is_allowed()

    Fully agree.

  3. We already have hook_scheduler_nid_list() and hook_scheduler_nid_list_alter(). We will need to explain the difference and uses of the now three hook functions, so that module developers understand. Does this kind of documentation go in module onscreen help, or in a readme file?

    Let's put it in a scheduler.api.php file, with some examples. There's already an issue about this: #1979462: Provide API documentation.

  4. I'll address your remarks 4-5-6.

jonathan1055’s picture

For the function name, 'is_allowed' is the wrong part of speech. It is passive, I think it should be a verb, ie. hook_scheduler_allow_...()

Also we should avoid the word 'action', as this will cause confusion with actions that Scheduler (will soon) provide as part of the Rules integration.

So some suggestions are, in php to give a flavour of how they will look:

hook_scheduler_allow_activity($node, $action);
hook_scheduler_allow_operation($node, $action);
hook_scheduler_allow_process($node, $action);

Alternatively, we could code it such that it does not use a second parameter, and the function name includes the action. Then modules would implement:

hook_scheduler_allow_publish($node);
hook_scheduler_allow_unpublish($node);
// or even
hook_scheduler_allow_publishing($node);
hook_scheduler_allow_unpublishing($node);  

We need to make it easy for other modules to implement the hooks, so using the two functions makes the names simple to remember and to understand. If we did this, then the internal function could be called _scheduler_allow() and would look like:

function _scheduler_allow($node, $action) {
  // Check if the requested action has been scheduled for the given node.
  $key = $action . '_on';
  $result = !empty($node->$key);

  // Allow other modules to prevent the action on the given node.
  $hook_name = 'scheduler_allow_' . $action . 'ing';
  foreach (module_implements($hook_name) as $module) {
    $function = $module . '_' . $hook_name;
    $result = $result && $function($node);
  }

  return $result;
}

Regarding the return value, I would say keep it simple as a boolean. In Scheduler we will not know whether the calling module has displayed those returned message(s) for the violations. If we just get back a boolean then it is clear to those who are writing their hook implementations that they need to display the messages.

pfrenssen’s picture

hook_scheduler_allow_publish($node);
hook_scheduler_allow_unpublish($node);
// or even
hook_scheduler_allow_publishing($node);
hook_scheduler_allow_unpublishing($node);

I like this! Even though it means that module developers potentially have to implement two hooks, but this is a familiar pattern in D7 anyway.

What do you think of turning it around?

hook_scheduler_prevent_publishing($node);
hook_scheduler_prevent_unpublishing($node);
// or
hook_scheduler_deny_publishing($node);
hook_scheduler_deny_unpublishing($node);

It's ok for me to return a boolean and handle the messages in the hook. Ideally the messages should only be shown when a form has been submitted, but showing them on node save is already the current behaviour.

jonathan1055’s picture

Yes I think turning it round is even better, as that matches why the developer is implementing the hook - specifically to stop publishing. The default behaviour with no hook is to allow publishing. Good idea!

I have a slight preference for hook_scheduler_prevent_publishing()

pfrenssen’s picture

Status: Needs work » Needs review
StatusFileSize
new14.57 KB
new8.14 KB
new13.03 KB

Here's an updated version of the patch containing my work in progress over the past couple of days. It is nearing completion.

I have adressed most of the remarks. Setting this to Needs review for the bot, but it still needs some work.

Remaining work:

  • Test the creation of scheduled nodes through the interface so we can check if the correct messages are displayed.
  • Rename _scheduler_is_allowed() to _scheduler_activity_is_allowed().
pfrenssen’s picture

I renamed the hooks to hook_scheduler_prevent_(un)publishing(). Now it is maybe a bit weird to return FALSE if the (un)publication should be prevented, and TRUE if it is allowed. I left it like this because it is more common to use FALSE to deny something, but I would like a second opinion on this.

jonathan1055’s picture

Now it is maybe a bit weird to return FALSE if the (un)publication should be prevented

After the previous discussion, I thought again, and came to exactly the same conclusion. Having a negative purpose (prevent/deny) in the function name is problematic when returning a TRUE/FALSE. I believe it will be clearer if we reverted to hook_scheduler_allow_publishing() and hook_scheduler_allow_unpublishing() returning TRUE if allowed and FALSE if denied.

The internal function could be called _scheduler_allow($node, $action) and the code is more or less the same as you currently have. I just changed the comments slightly:

function _scheduler_allow($node, $action) {
  // Check if the requested action has been scheduled for the given node.
  $key = $action . '_on';
  $result = !empty($node->$key);

  // Check that other modules also allow the action.
  $hook = 'scheduler_allow_' . $action . 'ing';
  foreach (module_implements($hook) as $module) {
    $function = $module . '_' . $hook;
    $result &= $function($node);
  }

  return $result;
}

In the calls to this function, in _scheduler_publish() and _scheduler_unpublish(), it could be

    // Check that other modules allow the action on this node.
    if (!_scheduler_allow($n, $action)) {
      continue;
    }

Everything else is great. If you agree with the names as above, then we are RTBC. If you want to get the majority of this committed now then that's ok with me, and you could finish off the tests later in the issue. It's up to you.

Jonathan

pfrenssen’s picture

Assigned: pfrenssen » Unassigned
Issue tags: -Needs tests
StatusFileSize
new14.28 KB
new8.47 KB
new4.44 KB

Finally found some time to work a bit on Scheduler :)

I have addressed the remarks from #24. I changed my mind on writing a whole new test to just check if the messages are displayed when the hooks are implemented. We are actually already testing that the hooks fire correctly, so let's just assume that drupal_set_message() works as intended :)

Unassigning myself, feel that my work is done here :)

The last submitted patch, 25: 1979460-25-scheduler-allow_scheduling-test_only.patch, failed testing.

jonathan1055’s picture

Status: Needs review » Reviewed & tested by the community

I agree about the tests.
Good work. Excellent. RTBC :-)

It will be interesting, also, when you make this commit, to see if the -dev release gets updated. There are six commits which have been done since 18th Nov, and they are not showing yet on https://drupal.org/node/3292/commits

pfrenssen’s picture

Status: Reviewed & tested by the community » Fixed

I'm curious about that too!

Committed to 7.x-1.x: commit 3940a805b.

eric_a’s picture

It will be interesting, also, when you make this commit, to see if the -dev release gets updated. There are six commits which have been done since 18th Nov, and they are not showing yet on

I deleted the 7.x-1.x branch test yesterday and today there's a new dev release! (Must be related to the d.o. upgrade.) Chances are that a next commit will fix the commit views display as well.

pfrenssen’s picture

Thanks for the info! The dev release was initiated manually today (ref. #2158069: Development release not packaged after commits to Scheduler). The issue about the commit view is tracked at #2125405: Not all commits showing up.

Status: Fixed » Closed (fixed)

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

jonathan1055’s picture

This issue is an example of #2146913: Issue auto-closing logic does not consider comment activity. Pieter commented only 6 days ago but the last node change was 14 days ago, hence the auto-closure now.

I have written a patch to revert back to the required D6 behavior, but it's not been reviewed yet.

Jonathan