HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Any example of using the new sort & filter module?

rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one"NY ✭✭✭

@Link wrote in the initial change list for 2.3 "Add discussions sort & filter module for developers."

I wonder if this feature is used anywhere and how it can be used.

«1

Comments

  • R_JR_J Ex-Fanboy Munich Admin

    What I think is meant is that there are now some sort of "sort keys" available. Look at this:

    // Show the available sort keys and their definition.
    decho(DiscussionModel::getAllowedSorts());
    // Add a new sort key
    DiscussionModel::addSort(
        'watched',
        'Most Viewed by Category',
        [
            'CategoryID' => 'asc',
            'CountViews' => 'desc'
        ]
    );
    // Show again the allowed sort keys
    decho(DiscussionModel::getAllowedSorts());
    
    // Apply our new sort key
    $discussionModel = new DiscussionModel();
    $discussionModel->setSort('watched');
    
    // See the result!
    decho($discussionModel->get());
    

    If you create and apply a custom sort key in a DiscussionModels AfterDiscussionSummaryQuery event handler, you are able to sort the result any way you like.

    I remember we have spoken about the problem that you couldn't have individual sort directions before. This is a comfortable solution!


    You can do some more nice things, try that:

    DiscussionModel::addSort(
        'watched',
        'Most Viewed by Category',
        [
            'CategoryID' => 'asc',
            'CountViews' => 'desc'
        ],
        [3, 6, 11]
    );
    

    That would make that sort only apply to categories 3, 6, 11


    And even some fun stuff like this:

    public function discussionModel_afterDiscussionSummaryQuery_handler($sender, $args) {
        $sender->addSort(
            'bookmarked',
            'Bookmarked',
            [
                'w.Bookmarked' => 'desc',
                'DateLastComment' => 'desc'
            ]
        );
        $sender->setSort('bookmarked');
    }
    

    To put it short: without further changes you are able to sort by any column from tables Discussion and UserDiscussion

  • R_JR_J Ex-Fanboy Munich Admin

    Although that can be used like that, I guess I'm on the wrong track. I found this in the source code:

        /**
         * @var array The sorts that are accessible via GET. Each sort corresponds with an order by clause.
         *
         * Each sort in the array has the following properties:
         * - **key**: string - The key name of the sort. Appears in the query string, should be url-friendly.
         * - **name**: string - The display name of the sort.
         * - **orderBy**: string - An array indicating order by fields and their directions in the format: array('field1' => 'direction', 'field2' => 'direction')
         */
        protected static $allowedSorts = array(
            'hot' => array('key' => 'hot', 'name' => 'Hot', 'orderBy' => array('DateLastComment' => 'desc')),
            'top' => array('key' => 'top', 'name' => 'Top', 'orderBy' => array('Score' => 'desc', 'DateInserted' => 'desc')),
            'new' => array('key' => 'new', 'name' => 'New', 'orderBy' => array('DateInserted' => 'desc'))
        );
    
        /**
         * @var array The filters that are accessible via GET. Each filter corresponds with a where clause. You can have multiple
         * filter sets. Every filter must be added to a filter set.
         *
         * Each filter set has the following properties:
         * - **key**: string - The key name of the filter set. Appears in the query string, should be url-friendly.
         * - **name**: string - The display name of the filter set. Usually appears in the UI.
         * - **filters**: array - The filters in the set.
         *
         * Each filter in the array has the following properties:
         * - **key**: string - The key name of the filter. Appears in the query string, should be url-friendly.
         * - **setKey**: string - The key name of the filter set.
         * - **name**: string - The display name of the filter. Usually appears as an option in the UI.
         * - **where**: string - The where array query to execute for the filter. Uses
         * - **group**: string - (optional) The dropdown module can group together any items with the same group name.
         */
        protected static $allowedFilters = [];
    

    So they offer a way to use them as predefined request query parameters (although I haven't looked at how to apply them). But based on that comments you would be able to define a sort and a filter and the result of a http request would return discussions based on the provided filter and the provided sort key.

    I assume it is used in or useful for the API, but honestly I don't know and that is only wild guessing...

  • R_JR_J Ex-Fanboy Munich Admin

    Oh yes, it is already working here: https://vanillaforums.org/discussions?sort=new

  • This is excellent, @R_J .

    But can we sort by Categories and tags?

    site/categories/books?tag=tragedy

    So that discussions will be shown only if they fall in the category and tutorial.

    This would be phenomenal

  • R_JR_J Ex-Fanboy Munich Admin

    The DiscussionModel supports "sorts" and "filters". if you say it should "only show" we are talking about filters. While there are default sorts defined in the DiscussionModel, there are no filters.

    You would have to create a plugin that would consume site/discussions?category=books&tag=tragedy and add the corresponding filters to the DiscussionModel.

    When looking at the DiscussionModel, you'd find multiple methods that are dealing with filters.

    Although I'd say that doing such a restriction by using filters would be the best approach (it is not the only way to do so), I wouldn't see the best "starting point". When restricting what the /discussions show, I'd normally use discussionModel_beforeGet_handler, but this is to late in program flow. Based on that comment:

       /**
        * Get the registered filters.
        *
        * This method must never be called before plugins initialisation.
        *
        * @return array The current filter array.
        */
       public static function getAllowedFilters() {
    

    I'd try my luck with __construct and DiscussionModel::instance()->...

    Sorry, I know this all sounds like Chinese if you are not used to writing plugins for Vanilla but I do not know about your skills. All in all it shouldn't be that hard. If you don't feel like trying it out by yourself I might be able to throw something together later this weekend...

  • R_JR_J Ex-Fanboy Munich Admin

    Well, having such a filter is one aspect. You would need to think about how to let the user choose something like that and implementing such an interfece would be the real task here.

  • The UI part and implementation is the ease and dream part. But finding the file/code to piggyback on is the trick I have been digging for all night .... trying to see what handles the request.

    Here is an example https://meta.discourse.org/tags/c/installation/sso



  • R_JR_J Ex-Fanboy Munich Admin

    I found out that a filter wouldn't be the best approach - possible but not so clever. I'll try to assemble something based on the BeforeGet event and show it to you

  • charrondevcharrondev Developer Lead (PHP, JS) Montreal Vanilla Staff

    We already have a way to sort by tagged. It’s built in but we don’t have much UI for it. Example here


  • @charrondev is that the native feature or did I miss something?

    IIRC, that sort, is site wide, it will list all discussions from all categories.

    I am dreaming some door that opens all kinds of possibilities from url parameters like these....

    site/discussions?category=books&tag=tragedy

    site/discussions?category=drama&tag=tragedy

    site/discussions?category=books&tag=comedy

    site/discussions?category=drama&tag=comedy

    site/discussions?category=cms&tag=php&sort=mine

    site/discussions?category=books&tag=tragedy&sort=unread

    site/discussions?category=books&tag=tragedy&sort=top

    site/discussions?category=books&tag=tragedy&sort=unread

  • R_JR_J Ex-Fanboy Munich Admin

    With 50% already provided by Vanilla, you will only have to implement this part:


       public function discussionModel_beforeGet_handler($sender, $args) {
           $categoryRequested = Gdn::request()->get('category');
           if (!$categoryRequested) {
               return;
           }
    
           $allCategories = CategoryModel::categories();
           $categoryID = false;
           foreach ($allCategories as $key => $category) {
               if ($category['PermsDiscussionsView'] == false) {
                   continue;
               }
               if ($category['UrlCode'] !== $categoryRequested) {
                   continue;
               }
               $categoryID = $category['CategoryID'];
               break;
           }
    
           if (!$categoryID) {
               throw new Garden\Web\Exception\NotFoundException('Category');
           }
    
           $args['Wheres'] = ['d.CategoryID' => $categoryID];
       }
    


    Now you only need the UI part which will builds requests like site/discussions/tagged/TheTagName?category=TheCategorySlug&sort=new


    For sort=mine and sort=unread it would require some more work

  • So, this went from necro

    to pure hot baking sizzling explosive vibrancy


  • R_JR_J Ex-Fanboy Munich Admin

    I once created a dropdown on that page (for another reason). This plugin might be helpful: https://open.vanillaforums.com/addon/latest-plugin


    You would need to get CategoriesModel::categories() and build a dropdown consisting of the Name and UrlCode column

  • charrondevcharrondev Developer Lead (PHP, JS) Montreal Vanilla Staff

    @donshakespeare It would be awesome to get this type of thing in core but I fear we won't be able to do until we make some backwards incompatible changes around the endpoint. The constant challenge with these long-standing pages is that because of how unstructured the old event system is, it's incredibly difficult to refactor.

    We've had these types of filters in some internal mockups for a while, but when we do put them in core, I would expect them to be part of an opt-in flag for a totally new discussions page that starts over with some more structured & efficient methods of hooking in.

    In the meantime @R_J has you on the right track with this I think. I'll see what I can do with the discussion as far as getting rid of the necro and moving the category.

  • @R_J a slight correction

    $args['Wheres'] = ['d.CategoryID' => $categoryID]
    

    would inadvertently replace all the existing data in Wheres like the crucial d.DiscussionID

    I mended it to

    $args['Wheres']['d.CategoryID'] = $categoryID;
    

    You have done a marvelous deed today. Cheers!

  • rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one" NY ✭✭✭

    @donshakespeare - I just saw this discussion. All of @R_J 's suggestions are terrific and I have learned a lot from him. You may get some ideas from a very old plugin I wrote long ago called filterdiscussions long before Vanilla provided some builtin support for this. If you run on 2.8 you will need to change the following line from:

    class FilterDiscussion extends Gdn_Plugin {

    to

    class FilterDiscussionplugin extends Gdn_Plugin {

    Then, please read the long comment in the plugin source - there is no other way you will know how to actually use it...

    Eventually you will be able to filter discussions (just filter, not sort) based on any field in the discussion table (including columns added by other plugins). The plugin takes parameters from the url in a specific syntax, parses them, and adjust the sql parameters to obtain the desired filter. Since it is unlikely that end users will use that syntax, the real aim of the plugin is to be invoked by other plugins (e.g. addmenuitems) that internally use that syntax.

    Hope this will give you some inspiration.

  • @rbrahmson yes I agree with you on @R_J . I would like to have what he eats for breakfast. I am sure it is nothing short of pure clear code.

    As for your FilterDiscussion, it just blew out of the water, and I am not sure where I have landed. This thing is pure magic. First, is it safe!? I am still shaking ...

    Filtering is so important for a big forum and this plugin even beats some search mechanics.

    I will build a little UI for it to help my users fine-tune their search. Awesome job!

  • rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one" NY ✭✭✭

    Before you go and write more customization take a look at the saved filters option that does not reveal the parameters. Then take a look at the sidepanel links plugin as ways to invoke the saved filters. The combination of saved filters and sidepanel links may already give you what you need.

  • rbrahmsonrbrahmson "You may say I'm a dreamer / But I'm not the only one" NY ✭✭✭
    edited March 2019

    Also, as for safety, in addition to hiding the syntax in named saved filters you can control which columns can be used as filters through the plugin settings.

  • Yes, the "saved filters" is simply genius, well thought ahead on that one!

    Also, just in case, I replaced all $_GET with Gdn::request()->get() in the plugin.

    Also ...

    I have this saved filter... Tags=EQ:6&CategoryID=NE:321&InsertUserID=EQ:{userid}.

    Accessed by /discussions/FilterDiscussion?!filter=mine

    And then I was gonna ask if there was a way to make the filter accept dynamic stuff? And then I saw that function you exposed .... that's just brilliant!


    So I am doing

    $Addfilter = str_replace('{userid}', Gdn::Session()->UserID, $Addfilter);
    

    In this function

    if (!function_exists('GetSavedFilter')) {
       	// Display handle debug parameter
    	function GetSavedFilter($key,$value,$Debug = FALSE) {
    		if ($Debug) {
    			echo '<BR>'.__FUNCTION__.__LINE__.' key:'.$key.' Value:'.$value.' <br>';
    		}
    		$Addfilter = '';
    		for ($x = 1; $x <= 20; $x++) {		
    			if (trim($value) == trim(Gdn::config('Plugins.FilterDiscussion.SavedName'.$x,' '))) {
    				$Addfilter = trim(Gdn::config('Plugins.FilterDiscussion.SavedFilter'.$x,' '));
    				if ($Debug) echo '<BR>'.__FUNCTION__.__LINE__.' i:'.$x.' Addfilter:'.$Addfilter.' <br>';
    				if(Gdn::Session() && strpos($Addfilter, '{userid}') !==false){
    					$Addfilter = str_replace('{userid}', Gdn::Session()->UserID, $Addfilter);
    				}
    				return $Addfilter;
    			}
    		}
    		return $Addfilter;
    	}
    }
    
Sign In or Register to comment.