I think we need to start thinking about this too.
What I am certain we need: Table pm_tags with 2 columns: tag_name and tag_id.
Less certain:
Approach 1:
Support to mark tags in pm_index.
Pro's of former:
less tables, meaning less JOIN's.
Cons of former:
How do you support multiple tags? multiple rows of data?
Approach 2:
new table "pm_tags_index" with the following columns: tag_id, uid, thread_id.
Pros: getting the threads per user by tag should be simple.
cons: if we go with my schema, we need to JOIN pm_message (for MAX mid, subject, MAX timestamp), pm_index (for deleted) and also PER TAG (if we are using AND and not OR which is what IO assuem it to be...) pm_tags (to get the tag_id from the name) and pm_tags_index (to get the uid, tag_id and thread_id).
So that is start off with a JOIN between two tables, and keep adding two more per tag.
Any ideas if this is a good architecture or if we should use something else? if so, what?
| Comment | File | Size | Author |
|---|---|---|---|
| #50 | privatemsg_filter.zip | 4.47 KB | berdir |
| #38 | privatemsg_filter.zip | 4.42 KB | naheemsays |
| #37 | privatemsg_filter.zip | 4.42 KB | berdir |
| #36 | privatemsg_filter.zip | 4.4 KB | berdir |
| #31 | privatemsg_filter.zip | 4.09 KB | naheemsays |
Comments
Comment #1
naheemsays commentedThis may not work (meaning it is untested), but proof of concept that currently supports a single tag, akin to a folder in drupal 5 version of privatemsg.
EDIT - just a note that this patch goes after a rearchitecture patch from #311541: Freeze privatemsg db schema + msg delete functionality + changes to query builder - it should not matter which one is chosen.
Comment #2
naheemsays commentedupdated patch fixes a few things. but it still does not work. Not sure why.
Ideas?
Comment #3
litwol commentedI am in favor of approach 2.
We can always optimize it later in the game once we have a very good understanding of this functionality. plus approach 2 sounds more flexible.
Comment #4
naheemsays commentedyeah, that is what the non-working patch is trying to implement. (got a few ideas on what to try next, just need to get the time, which I may not have for a few days)
PS is there a reason why in the query builder, inner_join is called so? it seems to me that just calling it "join" would be enough as in the builder, the logic is taken are from what is in the fragment instead of adding its own
/drats - I think my keyboard is failing.
PS - if the lack of UI in my last few patches is puzzling - I want to get the schema/architecture functionality out of the way before attempting to tackle that.
Comment #5
naheemsays commentedand now a working patch.
I had over complicated things too - there is no need to add a JOIN per tag name per row - this can be done before hand and just the tag_id fed into the query.
The current patch can also be expanded easily to allow multiple tags, but lets leave that til later.
Comment #6
litwol commentedI wonder if we should leverage taxonomy module tagging functionality for privatemsg. that way we dont have to deal with categorization and filtering and only provide integration.
what do you think?
Comment #7
naheemsays commentedThinking out aloud:
Going that way should remove one table from privatemsg - pm_tags. pm_tags_index should probably be renamed to term_privatemsg to go with the term_node
After that, are there any specific functions of taxonomy we can use? Since the messages are not nodes, I do not see how the taxonomy module can be leveraged too much since the queries will be similar to before but just with differently names tables.)
On the other hand, if all it does is provide a (good) UI for adding and removing tags, it is worth it.
EDIT - I do not have time til probably Saturday to look into this, would love it if someone was willing to beat me to the punch :P
Comment #8
naheemsays commentedTwo patches:
1. Builds upon the other patches that are in code needs review status.
2. Includes them into a single combined patch - easier to test.
Comment #9
naheemsays commentedAnother combined patch... adds a select form to the privatemsg_list page, and has all tags namespaced under messages/tag/%
I know we have decided to not go this way, just needed to finish what I had started.
From the two tags I have tested, "important" still does not work, but "test" does.
Comment #10
naheemsays commentedand the right patch :P
Comment #11
naheemsays commentedok, same as the above, but the stuff is working.
Once again, two patches - one combined which works on current HEAD adding all the stuff I am working on and one "normal" which only includes the tagging stuff, but may not apply cleanly as it was built off a privatemsg that has been modified.
Current bugs:
privatemsg_privatemsg_list_alter is given incorrect account info when there is a tag in privatemsg_list, leaving a UID = 0, giving incorrect results.
For those testing, please note that this is not the final form in which the tagging feature will appear. the tagging will be broken out into its own submodule. The current version just allows easy testing.
Comment #12
naheemsays commentedpm_tags as a separate module. It needs a small patch to privatemsg.module to set the proper UID, which is also attached.
TODO:
1. Hook things in. There are forms there to attach to the listings and the view messages pages, but they are not hooked in. Any quick hints on how to get this done without me having to actually learn stuff? I can cheat and use conditionals in privatemsg to only show the tagging UI if the other module is enabled (which I may have to do for the user_blocking stuff in one place...).
2. Add a method to add more tags.
3. Do we want any tags to be created on install?
Comment #13
naheemsays commentedoh and before I forget, the above sub-module supports tag chaining: tag1+tag2+tag3 should give results where all three tags are used.
Comment #14
berdir1:
As you said, one way to do it would be to use module_exists('pm_tags') all over the place.. Easy to implement, but the own module doesn't really make sense then.. you could also add a configuration option and simply have a include file for all those tag functions..
About the other way, You've already named it:
Hook things in. Or: Provide your own hooks which can be used by other (sub-)modules. Possible hooks would be: insert, delete, select, read/view (list, thread, new message, block... ),This would of course be way more complex but would make it easier to add more sub-modules later. Actually, modules like notification could provide integration with privatemsg without the need to change both modules...
3: I don't think default tags do make sense as they depend on the type (and language!) of messages on a website
Comment #15
litwol commentedmodule_exists in this case is a bad solution. lets instead implement hooks which will make it easier for other modules to interact.
Comment #16
naheemsays commentedDoing it the quick and dirty way of module_exists now... and for some reason, the forms are not showing...
Comment #17
naheemsays commentedI have changed the format of the tags from being in the url to being arguments (tags=tag1+tag2+...) as discussed with litwol in irc yesterday.
With the hacky patch in the comment above, this actually does work, so setting as CNR. Not pretty, but it works.
Comment #18
naheemsays commentedupdate module - now it relies on #324406: refactoring of privatemsg_list (was: Make use of theme() functions in the inbox) for adding the form onto the privatemsg_list page.
This version also adds another page to list, add and delete tags.
Just a word of warning: the current module does not seem to display any results for the dropdown on the list page if themer.module (or it might be devel.module) is active...
Comment #19
naheemsays commentedok, near final now. I have used pm_tags_form_alter to add new fields to the privatemsg_new form on the message view page...
How ever, clicking submit gives a message:
and the tags are not saved. No idea why, or how to fix. Ideas?
Apart from that bug, I think this is (more or less) ready, along with the patch in the issue linked to in comment 18. (there is no need for the patch earlier in this issue.)
EDIT: that message of "result:0" also appear when replying to a message. Still no idea how to fix that and why the tagging info is not saved.
EDIT 2: Ignore that message - that is from somewhere else Just gone back to an earlier clean version of the privatemsg module and patched with patch from linked issue and this submodule and no longer get that message. It was probably some forgotten debug for something else I was testing.
The thread tagging functionality still does not seem to work but everything else does. Almost pointless being able to add tags, delete tags, filter by tags, but not actually tag messages. :(
Comment #20
naheemsays commentedAnd the (hopefully) final - fully working - version of this module.
To use:
1. Apply patch in #324406: refactoring of privatemsg_list (was: Make use of theme() functions in the inbox)
2. Upload this module.
3. Activate.
@litwol: too small for a patch, but we can now remove the "create new folder" permission from privatemsg.module... this module has finegrained permissions in its own right.
Comment #21
naheemsays commentedUpdated module to match the changes in #324406: refactoring of privatemsg_list (was: Make use of theme() functions in the inbox) comment 13.
EDIT - updated with a few more fixes - so that the tagging box will only show on message pages if there are tags to select.
Comment #22
berdirBecause you count the number of selectable tags inside pm_tags_dropdown, drupal_get_form still displays an empty form if there are no tags.
This adds some unecessary markup (and an empty line) to the site so I think it's better to check the tags befor calling drupal_get_form()
This should work
Apart from that, it does work for me but as you said some parts are not really pretty at the moment, especially the thread display. I'll have a look at it...
Comment #23
naheemsays commentedupdated module with above changes.
Also, I am passing through the account parameter, as that is what we want the stuff to be based on, not the global $user.
Also tested with the paging in HEAD - it works.
Comment #24
berdirUpdate for my change in [#/324406] and a simple E_NOTICE-fix*
* Yes, I'm developing with E_NOTICE on ;)
Comment #25
naheemsays commentedPatched module.
Comment #26
naheemsays commentedupdated module going after patch in #330419: Block empty replies + Alter function to add content to the view messages page + consistent use of $user object in forms
Comment #27
naheemsays commentedprobably a slightly better way to go.
I renamed the submodule to privatemsg_filter.
it still includes tagging support, but it can now also filter based upon authors (though there is no UI for this - you have to just type &author=username at the end of the url to test.)
Is there a good form example out there that ic an use to add the necessary UI?
WHat I am envisioning is a form with one drop down column with two options:
1. Filter by tags.
2. Filter by author.
choosing either one updates another drop down right next to it to show the correct options.
Keeping as cnr as even without UI to choose the author, the module works. It can accept unlimited tags (all separated via a +, but the UI only allows to filter by a single tag) and a single author.
Comment #28
Babalu commentedsubscribing
Comment #29
berdirThe only filter example that I know of[1] (atleast in core) is at admin/reports/dblog. I'm not sure how easily it can be adopted, but what you should take from it is multi-select and the reset button (yes of course, you can click messages again, but I have now idea how clear that is for an user).
[1] Update: Well, another examples are the (base) Advanced Search and the project advanced search pages. Speaking of search and filtering, why not add a genereric search field to search title and content, too...
Comment #30
berdirOk, here is my approach.... Handling of the filter form is quite similar to what dblog provides.
- PostgreSQL Support (Replaced " with ' for strings, removed backticks, changed the INNER JOIN so that it has a ON -> Depends on the PostgreSQL Support patch)
- UI provides now author and tag multi-selects and a generic search textfield (just for subject)
- Filters are stored in the session and there is a Reset-Button (As I said, similar to dblog)
- If atleast one filter option is active, the filter form is displayed by default, the title changed ("Filter Messages (Active)", better ideas ?) and the current filters are pre-selected
- GET-Parameters are still supported, they are checked and stored in the session at the beginning of the form building. Still sing " " (aka +) to delimit multiple authors/tags. And for authors and tags both the names and the Id's can be submitted, only the Id's are stored in the session. A GET-Parameter does reset (only) the respective filter type and is displayed in the filter form.
- Reworked the list_alter hook to use the SESSION values and IN "(x, y, ...)" instead of = x to support multiple tags and authors without multiple joins
Open/Unclear things...
- The current implementation of the subject search is not nice, % are hardcoded and have to be escaped twice to get past assembly_query and db_query (I don't like the current approach of assembly_query to insert the params with printf because that does not escape anything... )
- The usabilty needs to be improvided, It might be confusing for some users.
- Maybe reset a filter if no results are found (should be possible be checking it in the list_messages hook) ?
I'm attaching a diff too so that it is easier to see the changes.
Feedback?
Comment #31
naheemsays commentedLooks good.
I updated the module with:
1. Show the form even if there are no tagged messages - even if there are no used tags, the user may filter by author or use the search box.
2. Renamed the search box to Search Titles.
3. Only show the author select box if there are authors - maybe we can make this for the full form?
4. Remove the tag count function as it is no longer used.
As for the session stuff, I would prefer if it would use $_GET and display the tags/author name in the url, but that is just me. (the JOINS could probably still be got rid of, but I do not know much SQL at all.), but this causes problems over what characters can be used (problems with "#" in tags, titles etc etc).
There is currently a problem over the module "remembering" the filtering. Not sure if this is a real issue but I would prefer it if when the page is left, to clear the filter info. Otherwise, returning to the same page later will show the filtered output. Is this normal? its just not how I expected it to be.
Comment #32
berdir1. Good catch, that's what I actually wanted, but forgot to change. Maybe we should display the form only if there is atleast one message ($content['messages'] non-empty). In this case, we would also have atleast one author.
The "problem" with GET is that the form does send the values as an Array, so you end up with: ?tags[1]=tagX&tags[2]=tagY, this does not look nice so we would need to change that if we want a nice GET-URL... and change it back in the list-alter hook. And you don't have control over the values, so a user could change the values so that we need to re-check them every time (including loading the Id's if we display the names in the URL).
But it is possible with one JOIN and special characters can be escaped with urlenocde. It would however need one query more to get the ID's.
And if we store the filters in the session it is by default permament. I wanted to show that with the open form. This is also how it is done with dblog. Maybe we could provide both ways, something like:
[ Filter once ] [ Save Filter ] [ Reset Filter]
Comment #33
naheemsays commentedIs it not possible to modify the submit action to send out an imploded version? tags=x+y+z
and yes, it would lead to more queries as you mentioned. I just like hacing the ability to type in a url to get what I want...
This way, we could have the search field NOT remember its session, but the urls will mean that if someone wants to remember the other filter settings they can (but not in conjunction with the search...)
Comment #34
litwol commentedi like the idea of URL parameters because it will enable us to save filters and make them reusable later on. maybe even allow users to define default filter ? that will also enable us to study most common filtering that users do and be able to optimize based on that.
None of that needs (nor should) happen now, just some forward thinking of what URL arguments will enable us. good stuff guys :)
Comment #35
berdir> Is it not possible to modify the submit action to send out an imploded version? tags=x+y+z
Not directly, sending the form is the task of the browser. However, what would be possible is parse the sent values and rewrite them as a "easy to read" url and then do a drupal_goto, similar to what had you before.
>I just like hacing the ability to type in a url to get what I want...
It is currently possible to write/create Filter-URL's, but they are stored in the session after being accessed first. But I can see why it could be useful to display the url's.
I'll try to provide a improved version as soon as possible. I think the best way is to create a function like privatemsg_filter_get_filter() which checks Session, URL (and maybe at later point saved filters, see below) and returns the filter array so that the list_alter hook can work with it.
@litwol
Saving/storing filter sounds interesting, but is currently not that important imho. Some ideas for later..
- Save a filter with a name and display it somewhere (for exampe in the block)
- Add some special filter options as checkboxes, for example:
-- Display only unread messages
-- Display only unanswered messages
- Provide some default filters. This would for example allow us to (finally ;) ) remove the sent messages tab and instead provide a filter displaying the messages where the user is an author.
Comment #36
berdirOk, here is a new try.
Some changes:
- Button-Layout: [ Filter ] [ Save Filter ] [ Reset ] (Reset is only displayed when a Filter is active)
- GET-Values are no longer saved in the session
- Filters can now be used once as a GET-URL (Filter) or saved in the Session (Save Filter)
- Added a new method privatemsg_filter_get_filter, which returns GET or session parameters (If atleast one GET-Parameter is present, session is ignored)
Some things are not really nice, for example I am using the first query_args entry in list_alter as the user_id. And LIKE %...% is still very ugly, but I will open a new issue about query_args (Security related)
Comment #37
berdirForgot to add some of nbz's changes.
- Search -> Search Subjects
- Display the form if there are no tags used
- Display the form only if $content['list']['content'] is not empty or a filter is set
Comment #38
naheemsays commentedsetting to rtbc, as while there are probably bugs, it does look good enough to go in.
A question though:
I assume that is the reason why you put an array_unshift into the pgsql patch? if so, the above query would also break the mysql version without it. What I had done before, which IMO is still cleaner is:
This way the WHERE is always after all the JOIN's, so there should be no need for an array_unshift. (as an aside, is it a good idea to replace all the ON statements with WHERE?)
I have attached an updated module with above change.
The main bug I can see at it that input is not filtered, allowing tags containing characters such as
#- this works as a tag when the filter is saved, but when it is not saved, such that it shows and is loaded from the url, it is ignored/broken.Comment #39
litwol commentedAs clean as this solution may be i still cannot agree with the approach:
These are join points and therefore should be inside inner_join:
where as this is a conditional branch and should be inside where:
Comment #40
naheemsays commentedSo, is an array_unshift the way to go then? if so, the zip in comment 37 goes that way.
As for the final statement, I have no idea what you mean by it. does that where statement need to change too?
Another question with that last stated where query... does it change the tagging from an AND to an OR (Where the tag id matches any one of that array, instead of ALL of them)? I am quite sure we want an AND.
Comment #41
berdirAnd PostgreSQL *needs* an ON for an INNER JOIN, so this needs to be there. Replacing ON with WHERE is not a good idea in general because i'm quite sure it is slower and may lead to different results.
privatemsg_filter_get_filter should take care of security issues for now. author's and tags are used if they are present in $tag_data or $author_data and the search value is escaped.
Your example with # is actually not related to security, but everything behind # is not even sent to the server but is instead used by the browser to "jump" to an < a name="xy" >-tag, if present. And if you use for exampe ?tags=some#tag, the server will send a GET ...?tags=some... but some is not found in $tag_data and therefor ignored.
Comment #42
litwol commentedAlso i just realized something, code like this is a huge security risk:
we shouldnt place tags directly into the query. we must use proper %d and %s variable replacements to avoid sql injection attacks.
Comment #43
naheemsays commentedI know. Its just a matter of needing to filter the input data, allowing the correct characters.
Comment #44
berdir@litwol
Yes, I know. The downside of IN is that is not easily possible to use replacements because you don't know the number of elements there are. It is in this example however not a real security issue, because $filter comes from privatemsg_filter_get_filter which only returns GET-Parameters that are real tags used by this user ($tag_data) and transformed to the key. But there *is* a security problem, because the sql generating function does not use proper escaping either.. it simply inserts the arguments with vsprintf. I'll open a new issue about that as stated above.
However, nbz raised another question... do multiple authors/tags mean OR or AND ? If the latter, IN does not work anyway and I have to rewrite it so that there are multiple WHERE parts (Wich is not so complex, it's more a definition problem than a technical one)
And yes, with the ON we also need the array_unshift to be sure that the pm_index is always the first INNER JOIN.
Comment #45
litwol commentedThe reason i went with sprintf is that it seems to cast variable types, %s and %d. so i'm not sure of the security complications that you are referring to. also we cant predict what kind of query rewriting some one may do so its best we stick to standards.
to find out how many %s or %d we need is rather easy, you can use http://us.php.net/array_fill with count($filter['tags']) number of times.
Comment #46
berdirYes, sprintf is fine for int values, but for strings, casting is not enough, we need to escape the strings...
To test it, simply remove the db_escape_string call in privatemsg_filter_get_filter and search for something like " string with ' ". (the search string *does* use %s).
This will produce the following sql error:
Comment #47
litwol commentedYou are absolutely right, in that case we must replace using sprintf with :
Comment #48
berdirCreated a patch of your code, see #339565: Security issue with _privatemsg_assemple_query
IN means "is one of them" => OR
If we want AND I have to change that query anyway.
I'm not sure what to do... any similar drupal form i know (dblog -> type, issue search -> status) does use OR *but* an issue can have only one status, a log entry only one type and so on. But a thread can have multiple tags and authors, so it does probably make sense to use AND.
Comment #49
naheemsays commentedyup, as far as I know, we want AND as we are filtering modules. So does this take us back to the original method of using JOIN's?
The version in comment 27 (without many of the later improvements) uses AND, but has the limitation of having a maximum of one author to search by.
Comment #50
berdirYes, it seems so.
Here is a updated zip.
Changes:
- Re-added the multiple joins code, a bit modified however... it does use ON instead of WHERE's
- Also doing multiple joins with the users table to have a similiar functionalily for author filtering
- Added primary keys for pm_tags and pm_tags_index.
- Slightly changed the search parameter so that the % only have to be escaped once.
One thing I'm asking me is if we should allow private/personalized tags, so that each user could have his own set of tags. This would effectivly mean to move the uid attribute from the pm_tags_index table to pm_tags. Just an idea...
Comment #51
naheemsays commentedIt works and had a quick look at the code changes - they look good to me.
as for having per user tags - that could quickly go down the road where many people have recreated teh same tags over and over again.
There may be a benefit to doing it that was (as the Drupal 5 version of privatesmsg did that for folders), but I cannot see what it is - it just stores extra data.
One benefit of the current system is that it allows only giving one role the permissions to add/remove tags, but others can still use them. It could work well for workflow/task based needs.
Comment #52
litwol commentedThank you all for hard work. its been commited now: http://drupal.org/project/cvs/3279