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.)
Comment | File | Size | Author |
---|---|---|---|
#36 | flag-have_hook_flag_validate-952114-36.patch | 14.6 KB | acrollet |
#34 | flag-have_hook_flag_validate-952114-34.patch | 14.6 KB | acrollet |
#32 | flag-have_hook_flag_validate-952114-32.patch | 14.26 KB | acrollet |
#28 | flag-have_hook_flag_validate-952114-28.patch | 14.23 KB | acrollet |
#25 | flag-952114-error-messages-overflow-1.png | 25.72 KB | joachim |
Comments
Comment #1
mooffie CreditAttribution: mooffie commentedI'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.
Comment #2
jeff.k CreditAttribution: jeff.k commented+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.
Comment #3
mooffie CreditAttribution: mooffie commentedThere's another advantage to this feature:
Implementing hook_flag_validate() will be considerably easier than implementing hook_flag_access(), for two reasons:
Comment #4
quicksketch@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.
Comment #5
YK85 CreditAttribution: YK85 commented+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.
Comment #6
mooffie CreditAttribution: mooffie commentedI'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:
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 byhook_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:
You may also want to add the following to your stylesheet:
(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)
Comment #7
mooffie CreditAttribution: mooffie commentedyaz085 wrote:
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.)
Comment #8
mooffie CreditAttribution: mooffie commentedMarking #974254: How to avoid flagging based on condition that a node with same term is flagged a duplicate of this issue.
Comment #9
tobiberlinsubscribing
Comment #10
Rubix CreditAttribution: Rubix commentedHello!
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. :)
Comment #11
mooffie CreditAttribution: mooffie commentedYes, 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".
Comment #12
Rubix CreditAttribution: Rubix commentedI 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:
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.
Comment #13
rbayliss CreditAttribution: rbayliss commentedI 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!
Comment #14
rbayliss CreditAttribution: rbayliss commentedOne 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?
Comment #15
mooffie CreditAttribution: mooffie commentedRob, thanks for the review.
For hook_flag_validate() I followed the signature of hook_flag().
(Interesting. I'll later investigate why hook_flag_access() has a different signature.)
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.)
Comment #16
geerlingguy CreditAttribution: geerlingguy commentedSubscribe. Any idea as to whether we'd get this into D7 soon too?
Comment #17
guillaumev CreditAttribution: guillaumev commentedHi,
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
Comment #18
jeff.k CreditAttribution: jeff.k commentedDoes anyone have a working patch for Beta6? The patch above seems to only work up to beta 4
Comment #19
joachim CreditAttribution: joachim commentedMarking as needs work. Also, needs to be applied to D7 first, then backported.
Comment #20
joachim CreditAttribution: joachim commentedAlso, any new hook needs documenting in the api.php file.
Comment #21
joachim CreditAttribution: joachim commentedTagging.
Comment #22
snufkin CreditAttribution: snufkin commentedRerolled for D7 dev.
Comment #23
joachim CreditAttribution: joachim commentedUgh, 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! :)
Comment #24
acrollet CreditAttribution: acrollet commentedI 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?
Comment #25
joachim CreditAttribution: joachim commentedThanks 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?
Here's a few things I've spotted that need cleaning up or seem a bit funny:
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.
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.
We removed this message in flag_page() (AFAICT!), so it should be removed in the other places too presumably?
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:
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.
Comment #26
joachim CreditAttribution: joachim commentedI'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:
With exceptions param:
With get_errors() method:
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 ;)
Comment #27
acrollet CreditAttribution: acrollet commentedMany 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.
Comment #28
acrollet CreditAttribution: acrollet commentedpatch 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!
Comment #29
joachim CreditAttribution: joachim commentedLooking good. Just a few more minor things to tidy up.
Instead of 'test', can we say 'The action about to be carried out' here?
'The id of the entity the user is trying to flag or unflag.'
Good stuff on the example code. We need a @return here too.
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:
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.
Comment #30
joachim CreditAttribution: joachim commentedRegarding the CSS, could you compare with #1620614: .flag-message styling?
Comment #31
acrollet CreditAttribution: acrollet commentedre-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...
Comment #32
acrollet CreditAttribution: acrollet commentedwhoops, forgot to actually attach the patch after all that!
Comment #33
joachim CreditAttribution: joachim commentedThe $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.
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.
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!)
Comment #34
acrollet CreditAttribution: acrollet commentedre-rolled.
Comment #36
acrollet CreditAttribution: acrollet commentedre-rolled, should pass tests this time.
Comment #37
joachim CreditAttribution: joachim commentedLooks 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().