Right now we have a hook_flag_acees() hook that gives us functionality somewhat akin to that of Drupal's node access.

That feature isn't bad. However, it turns out that there are a lot of use-cases that this "node access" paradigm can't solve:

There are cases where we want to show the flag link, and, after clicking this link, if we determine that flagging (or unflagging) shouldn't be allowed, we want to print, in a graceful manner, an error message to the user.

This can be solved by introducing a new hook, hook_flag_validate(), that will be called by the $flag->flag() method.

Here are several open issues that can be solved with, or with the help of, this proposed feature:

#951260: Flag as Registration / Signup .. and beyond
#814960: limiting how many nodes/content types a user can flag.
#855880: How to limit number of votes per day?
#885568: lock node from further flagging (changes)
#918508: Filter by date flagged (but he also wants to restrict flagging)
#812372: +1 vote system question
#913612: Like and dislike?
#285196: create multiple flags?
#472764: Limit flaggable nodes using views
#678812: Load flag template even for users without access

(Some of these issues on first glance may seem irrelevant to this discussion; One has to read through them to see that they do.)

Implementation

  • The $flag->flag() method will call this hook. Modules wishing to [conditionally] abort the flagging operation will implement this hook; They will return a message to show to the user in this case.
  • We will show this error message to the user in the same manner we show the "flagging message". This is important: we won't use a crud alert() box for this but display this message in nice HTML.

Rationale

These two paradigms, the "node access" one and the validation one complement each other. Both paradigms are used in Drupal itself. (There's another rationale, related to #871064: Making flaggings fieldable, but I want to keep this introduction short.)

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mooffie’s picture

I'll now be marking as "duplicate" many of the issues I linked to, because we can't give these people any other solution (the current hook_flag_access() isn't always applicable).

So if you see that most of the issues I listed are stricken out it doesn't mean they were solved: they are just linking here.

jeff.k’s picture

+1
I am using Flags + rules as a registration feature and right now I have to add the flag then remove it if I am over the number of registrations. hook_flag_validate() would make the process much simpler.

mooffie’s picture

There's another advantage to this feature:

Implementing hook_flag_validate() will be considerably easier than implementing hook_flag_access(), for two reasons:

  1. One won't have to implement two hooks (hook_flag_access() + hook_flag_access_multiple(), and on top of that their code is usually quite different: the latter uses SQL, for efficiency).
  2. Efficiency: When implementing hook_flag_access_multiple() one has to be careful to write efficient code. But there won't be such an issue with hook_flag_validate() because it will be called only when the user initiates the flagging.
quicksketch’s picture

@mooffie: I don't have the time necessary to fully implement (or even review) this proposal. However I trust in your judgment and it does sound like an excellent idea.

YK85’s picture

+1 subscribing. This would be great in allowing a maximum of 50 users to flag an event node as 'attend'. So if someone unflags, the link will then show again and another user will be able to flag the node.

mooffie’s picture

Status: Active » Needs review
FileSize
14.6 KB

I'm attaching the patch. It's a simple one (it's not very short, but it's not complex).

1. Example for implementing hook_flag_validate()

Let's begin with an example. Here's how to forbid a flagging operation:

/**
 * Implements hook_flag_validate().
 */
function MYMODULE_flag_validate($action, $flag, $content_id, $account) {
  if ($flag->content_type == 'node') {
    if ($content_id == 666 && $action == 'flag') {
      return t("You're not allowed to flag evil nodes!");
    }
  }
}

This hook is called using module_invoke_all() so it's also possible to return an array of error messages. (One can use meaningful identifiers for the keys of this array.)

2. Extension to the Flag API

When a flagging operation cannot be carried out, the $flag->flag() method now returns a list of error messages. Among these are the errors returned by hook_flag_validate() implementations. These errors are eventually printed out for the user in the flag template. This list is returned via a passed-by-reference variable.

This extension to the API is backward compatible. Old code will not break.

Comments:

- In modern code such errors are returned in an exception object, but raising exceptions will break existing code.

- The code "function whatever(&$var = 'something')" is PHP 5 only, but we've already settled on moving to PHP 5.

3. The JavaScript side

Previously we used a 'status' boolean field in the JSON structure to signal an error, which would be shown using alert(), but this is not needed anymore.

If custom JavaScript code wants to know whether there was an error, it can inspect the 'flagSuccess' field.

4. Our flag.tpl.php

The flag template hasn't really changed. The message text variable ($message_text) is used on both success and error. To differentiate between these two cases we use CSS classes on the message container: $message_classes. This allows the themer to show errors in red and success notifications in green, for example.

(Additionally, I've deprecated the $last_action variable in favor of $status, which is easier to understand. (Why? I'll explain later, if asked.))

5. The theme preprocess function

The code added here deals with building the $message_classes variable.

6. Testing the patch

After applying the patch (but apply this one first) and clearing Drupal's cache, put the following code (after renaming "MYMOUDLE") in a custom module:


/**
 * Implements hook_flag_validate().
 */
function MYMODULE_flag_validate($action, $flag, $content_id, $account) {
  if ($flag->content_type == 'node') {
    $node = $flag->fetch_content($content_id);
    if (strpos($node->title, 'days') !== FALSE) {
      return t("You may not flag a node with the word 'days' in its title.");
    }
  }
}

You may also want to add the following to your stylesheet:

/**
 * We don't want success and failure to look the same.
 */

.flag-success-message {
  background: #aaffaa;  /* greenish */
}

.flag-failure-message {
  background: #ffaaaa;  /* redish */
}

(Why am I using the word "failure", instead of "error", in the CSS? "error" seemed too harsh to me. But I'll eventually change it to "error" for uniformity.)

7. Ideas for contrib modules (not for Flag core)

  • It won't be hard to show the error message(s) in a modal dialog.
  • A module could expose hook_flag_validate() as Rules events. So admins will be able to forbid flagging (or unflagging) "without writing code".
mooffie’s picture

yaz085 wrote:

+1 subscribing. This would be great in allowing a maximum of 50 users to flag an event node as 'attend'. So if someone unflags, the link will then show again and another user will be able to flag the node.

You say "the link will then show again". No: the link will always show. It's just that an error message (e.g., "No more vacancies for this event") will be shown upon clicking.

(Though the link could be themed (or the link text be modified) to suggest full attendance. It's not hard. But let's not talk about this here: it'd only add noise to this thread.)

mooffie’s picture

tobiberlin’s picture

subscribing

Rubix’s picture

Hello!

I've tried the patch for a short while...seems to be working great except that the failure message doesn't fade out the same way flagged/unflagged messages do. :o (or is this by design?)

Haven't had the chance to investigate further yet. Just reporting if anybody else experienced this behavior. :)

mooffie’s picture

seems to be working great except that the failure message doesn't fade out the same way flagged/unflagged messages do. :o (or is this by design?)

Yes, it's by design.

I felt the out-of-the-box behavior should not be to make it disappear after 3 seconds, because the failure message may require the user attention. But I made it possible for a themer to override this decision (e.g., by adding the "flag-auto-remove" CSS class in a theme preprocess function; Or, for example, by using our JavaScript API to show this message in a modal dialog box (I planned to write a mini-module to do this)).

Now,

We can start a deep philosophical discussion about this. We can divide ourselves into three camps: those that believe we should show the message for 5 seconds, those that believe in 9 seconds, and those that believe it should be configurable from Flag's UI. But let's not do this: The only question is whether a themer/programmer is given the tools to do whatever he wishes, and I think the answer is "Yes".

Rubix’s picture

I see...yes, I agree that it should be configurable. If I may suggest, instead of returning a string being the failure message, an array may be returned containing the message and the duration of the display.

Maybe something like:

<?php
function MYMODULE_flag_validate($action, $flag, $content_id, $account) {
  if ($flag->content_type == 'node') {
    $node = $flag->fetch_content($content_id);
    if (strpos($node->title, 'days') !== FALSE) {
      return array(
        'message' => t("You may not flag a node with the word 'days' in its title."),
        'duration' => 10000 //milliseconds; 0 = do not fade out
      );
    }
  }
}
?>

I haven't looked into the code much if this would impact other things so forgive me if this isn't viable. :) I just think that this would be an easier/faster solution for people using this hook on his/her custom module, not to mention they can set different timeouts/behavior for the messages.

rbayliss’s picture

I didn't run into any issues with it working against the 6.0-2.0-beta5. I would vote to skip the validate hook if $skip_permission_check is true, since I could see that being confusing or a pain in the future. Thanks for a great module!

rbayliss’s picture

One more comment: Doesn't it make sense to keep the function signature consistent with hook_flag_access, since it's just a difference of reordering the arguments?

mooffie’s picture

Rob, thanks for the review.

One more comment: Doesn't it make sense to keep the function signature consistent with hook_flag_access, since it's just a difference of reordering the arguments?

For hook_flag_validate() I followed the signature of hook_flag().

(Interesting. I'll later investigate why hook_flag_access() has a different signature.)

I would vote to skip the validate hook if $skip_permission_check is true, since I could see that being confusing or a pain in the future.

Thanks for paying attention to this. I don't yet have an opinion on what's the "best" thing to do. (But we can postpone the decision on this so this shouldn't block us from committing this feature.)

geerlingguy’s picture

Subscribe. Any idea as to whether we'd get this into D7 soon too?

guillaumev’s picture

Hi,

I was interested in having this in Drupal 7 as well and I wrote a small patch which allows to add a simple check within hook_flag_validate, returning a boolean...

Note that I'm using this in Commerce Credits Flag

jeff.k’s picture

Does anyone have a working patch for Beta6? The patch above seems to only work up to beta 4

joachim’s picture

Version: 6.x-2.x-dev » 7.x-2.x-dev
Status: Needs review » Needs work
Issue tags: +Needs backport to D6

Marking as needs work. Also, needs to be applied to D7 first, then backported.

joachim’s picture

Also, any new hook needs documenting in the api.php file.

joachim’s picture

Issue tags: +Novice

Tagging.

snufkin’s picture

Status: Needs work » Needs review
FileSize
704 bytes

Rerolled for D7 dev.

joachim’s picture

Version: 7.x-2.x-dev » 7.x-3.x-dev
Status: Needs review » Needs work
Issue tags: -Novice

Ugh, sorry I should have read all of this issue before tagging it as Novice.

The patch at #17 is a cut-down version of the original patch, which only allows failure of validation and no messages.

Bumping version to 3.x. This is going to need involved work from someone who needs this feature! :)

acrollet’s picture

Status: Needs work » Needs review
FileSize
14.46 KB

I need this functionality, and am attaching a re-roll of the patch in #6 against 7.x-3.x-dev - it's working nicely for me, but obviously needs review. If it's ok with the maintainers, I'd like it if the "7.x-3.0 release blocker" tag could be added to this issue - thoughts?

joachim’s picture

Status: Needs review » Needs work
FileSize
25.72 KB

Thanks for working on this!

Regarding blocker status, I'm not sure I think this qualifies: we've been living without it for 2 years now, and while it opens the door to lots of cool new uses of Flag, I don't think it's an essential for a 3.0 release.

However... I doubt I'll be releasing 3.0 right away, as we still have a few blockers and things aren't moving fast. So you've definitely got a good few weeks (at least!) to get this patch ready. In other words, I'll consider this a 'nice to have' for 3.0 rather than a 'must have'.

It does need a bit of work though...

First up, a new hook needs documenting in the api.php file :)

I've given it a run through with a couple of dummy hooks returning errors, and while it works, it seems to me there's a problem when we return multiple error messages. One line of message text sneaks into the padding around a node content in most themes, but more than one and it overflows and looks messy. I think a bit of CSS is needed here -- maybe something like a box with a border around these do the trick?

flag-952114-error-messages-overflow-1.png

Here's a few things I've spotted that need cleaning up or seem a bit funny:

+++ b/flag.module
@@ -1604,9 +1604,17 @@ function flag_theme() {
+function theme_flag_errors($errors) {
+  // Note: by default we can't use <div> because it's not valid inside <span>.
+  return join('<br />', $errors);
+}

I don't think we need a whole new theme function just for joining up some strings. Should anyone really need this, they can do the work in the flag theme preprocessor.

+++ b/includes/flag.pages.inc
@@ -25,45 +26,40 @@ function flag_page($action, $flag, $entity_id) {
-    print drupal_json_encode(flag_build_javascript_info($flag, $entity_id));
+    print drupal_json_encode(flag_build_javascript_info($flag, $entity_id, $errors));

I'd like a comment here to say that the JS handles outputting errors, since there's no obvious handling of them there with JS enabled.

+++ b/includes/flag.pages.inc
@@ -122,20 +118,27 @@ function flag_confirm_submit(&$form, &$form_state) {
     drupal_set_message(t('You are not allowed to flag, or unflag, this content.'));

We removed this message in flag_page() (AFAICT!), so it should be removed in the other places too presumably?

+++ b/includes/flag.pages.inc
@@ -122,20 +118,27 @@ function flag_confirm_submit(&$form, &$form_state) {
+    $flag->flag($action, $entity_id, NULL, FALSE, NULL, $errors);

We've just got a $result from a flag() call, so why are we doing it all over again here?

Finally, I'm a bit concerned about adding yet another parameter to flag(). We've got $skip_permission_check, we recently added $flagging. The more parameters, the more unwieldy this gets, but that's just the first consequence.

Technically we should be adding $errors to the main API function, flag(), but then that means we need to tackle #1689530: flag() should support $skip_permission_check to make all the long list of parameters consistent. But then the long list of parameters means that using flag() and getting the errors means you need to pass in the defaults for all the optional params that come before it, which is rubbish DX: they effectively cease to be optional.

Hence I am wondering whether the errors should be handled using exceptions: we throw a FlagException in flag_flag::flag() if we get some errors from either our own code or the hook invocation.

On the other hand, using exceptions make using the API function fiddly too: with an $errors parameter you can just choose to ignore it if you don't want to return error output, but with exceptions you always have to use a try/catch block just in case.

It's still a bit too early in the morning to think this one through, so I'll ponder this one some more -- though any opinions welcome :)

PS. On the subject of lots of parameters:

    // @todo: Should we skip this if $skip_permission_check == TRUE ?

We should probably pass $skip_permission_check to the hook for implementations to figure it out.

Also, passing the $flagging to the hook is necessary I think. I can see implementations wanting to work with field values on that.

joachim’s picture

I've thought of a third option: we store an array of errors in the flag, and add a method to retrieve it.

Thus:

With $errors param:

// Don't care about errors.
$flag = flag_get_flag(...);
$flag->flag('flag', ID)

// Need to get errors.
$flag = flag_get_flag(...);
$errors = array();
$flag->flag('flag', ID, NULL, FALSE, NULL, $errors);
if ($errors) {
  // Output.
}

With exceptions param:

// Don't care about errors.
$flag = flag_get_flag(...);
try {
  $flag->flag('flag', ID)
}
catch (Exception $e) {}

// Need to get errors.
$flag = flag_get_flag(...);
try {
  $flag->flag('flag', ID)
}
catch (Exception $e) {}
  // Output.
}

With get_errors() method:

// Don't care about errors.
$flag = flag_get_flag(...);
$flag->flag('flag', ID)

// Need to get errors.
$flag = flag_get_flag(...);
$flag->flag('flag', ID, NULL, $errors);
if ($errors = $flag->get_errors()) {
  // Output.
}

It actually saves us a line of code when getting errors out, and requires no extra work if you don't care about errors, so it seems like the best way to me. It also means we get the $errors array documented -- I've not managed to spot where it's explained what the array keys do ;)

acrollet’s picture

Assigned: Unassigned » acrollet

Many thanks for the detailed feedback - I'll work on a re-roll in the next couple of days with the get_errors method, which seems like a nice way to go IMO.

acrollet’s picture

Assigned: acrollet » Unassigned
Status: Needs work » Needs review
FileSize
14.23 KB

patch attached incorporating the feedback in #25 and #26. FWIW, the hook documentation in flag.api.php includes an example implementation of the use case in #814960: limiting how many nodes/content types a user can flag.. Thanks for helping move this forward!

joachim’s picture

Status: Needs review » Needs work

Looking good. Just a few more minor things to tidy up.

+++ b/flag.api.php
@@ -121,6 +121,36 @@ function hook_flag_unflag($flag, $entity_id, $account, $flagging) {
+ *  The action to test. Either 'flag' or 'unflag'.

Instead of 'test', can we say 'The action about to be carried out' here?

+++ b/flag.api.php
@@ -121,6 +121,36 @@ function hook_flag_unflag($flag, $entity_id, $account, $flagging) {
+ *  The id of the entity the flag is on.

'The id of the entity the user is trying to flag or unflag.'

+++ b/flag.api.php
@@ -121,6 +121,36 @@ function hook_flag_unflag($flag, $entity_id, $account, $flagging) {
+ * @param $flagging
+ *  The flagging entity.
+ */
+function hook_flag_validate($action, $flag, $entity_id, $account, $skip_permission_check, $flagging) {

Good stuff on the example code. We need a @return here too.

+++ b/includes/flag.pages.inc
@@ -25,45 +25,40 @@ function flag_page($action, $flag, $entity_id) {
+    $flag->errors['token'] = t('Bad token. You seem to have followed an invalid link.');

I think I'd rather keep the errors array internal to the flag, so the errors we build up in flag_page() we just put in a local $errors array, like this:

$errors = array();
$errors['foo'] = 'bar';

$flag->stuff();
$errors += $flag->get_errors();

if ($errors) {
 // stuff
}
+++ b/theme/flag.css
@@ -9,6 +9,12 @@
+.flag-message.flag-failure-message {
+  border: 1px solid black;
+  background-color: #F5DCE6;
+  padding: 2px;
+}
+

I'm actually wondering now whether we could borrow the 'warning' class that Drupal core defines. That gives us a red border and a pale red background.

joachim’s picture

Regarding the CSS, could you compare with #1620614: .flag-message styling?

acrollet’s picture

Status: Needs work » Needs review

re-roll against latest HEAD attached, addressing the issues in #29. As regards the CSS, unfortunately I am not a CSS expert either. I've copied the colors from the core warning class, but I think that adding the warning class to the failure message div could make over-riding a bit messy. Would it be possible to get the hook in on its own merits and address the CSS issues in a follow-on issue? I'd be happy to ask for input from a front-end dev at my company...

acrollet’s picture

whoops, forgot to actually attach the patch after all that!

joachim’s picture

Status: Needs review » Needs work
Issue tags: -Needs backport to D6
+++ b/includes/flag.pages.inc
@@ -24,46 +24,43 @@ function flag_page($action, $flag, $entity_id) {
   // If successful, return data according to the request type.
   if ($js) {
     drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8');
     $flag->link_type = 'toggle';
+    // Any errors that have been set will be output below the flag link with javascript.
     print drupal_json_encode(flag_build_javascript_info($flag, $entity_id));
     drupal_exit();

The $errors we've already accumulated aren't taken into account -- so for example, even if the token is invalid, the flagging works.

I guess that means we have to set the errors we have here in the flag object's array after all.

  else {
    $flag->flag($action, $entity_id);
  }

I would rather this got its own 'if (!$errors)' -- as it is, it's tacked on the end of a multiple if/else statement, when really this is an operation in its own right. Would also be nice to have a comment that states that this attempt to perform the flagging may itself set errors.

    // Any errors that have been set will be output below the flag link with javascript.

This comment needs to be wrapped.

(Also, removing the 'needs backport to D6'. I strongly doubt anyone is going to work on backporting this to D6!)

acrollet’s picture

Status: Needs work » Needs review
FileSize
14.6 KB

re-rolled.

Status: Needs review » Needs work

The last submitted patch, flag-have_hook_flag_validate-952114-34.patch, failed testing.

acrollet’s picture

Status: Needs work » Needs review
FileSize
14.6 KB

re-rolled, should pass tests this time.

joachim’s picture

Status: Needs review » Fixed

Looks good. Let's get this in!

I think we need a follow-on to look at CSS styling of the messages. Though having just tried both the before and after versions of error messages, I do think the new way with the message box below the flag is a big UI improvement over the previous JS alert box.

Another thing to consider is that if you implement hook_flag_validate(), and add fields to your flag, and set your flag to use the form link type, you have a potential UX problem:

1. user clicks link to flag a node
2. user is prompted to enter field data for the flagging entity they are about to create
3. user is then told they can't flag the node -- they are returned to the original page they were at in step 1, and the data they have just spent time entering is lost.

Now it may be the case that field values form part of the checks in hook_flag_validate(). So the ideal solution of telling the user in step 2 that they can't flag isn't always desirable (and also it would be a total pain and a huge re-engineering undertaking to implement, as we'd need a way to 'test' the node is flaggable without actually flagging it).

But we still have the problem that we have lost the user's data: if the field values are checked as part of hook_flag_validate(), then the user should be given a chance to change them.

What this boils down to is that the call to $flag->get_errors() in flag_confirm_submit() should be in a flag_confirm_validate() instead. But as I say, I'm happy for that to be a follow-up, as it doesn't affect core flag operation, only a particular use case when implementing hook_flag_validate().

Status: Fixed » Closed (fixed)

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