Please upgrade here. These earlier versions are no longer being updated and have security issues.
HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Allow user to switch between discussions sorted by DateInserted and DateLastComment?

edited October 2015 in Vanilla 2.0 - 2.8

Is there a way to create a navigational link that allows the user to toggle whether the discussion list is sorted by DateInserted and DateLastComment?

For example: - displays discussions sorted by DateLastComment - displays discussions sorted by DateInserted

Sorry if I am missing something obvious here - been poking around previous discussions of "sort by date last created" and didn't come up with anything.


  • Options
    R_JR_J Ex-Fanboy Munich Admin

    There is no simple link for that. You have to create a plugin, but it should be fairly simply. If you like to, I could guide you.

    Start with that one: Name it any other way and use it as a blueprint.
    If you have it working, mimic the recent discussion page by copying the index() method of /applications/vanilla/controllers/class.discussionscontroller.php into the vanillaController_yourPluginName_create() function of the plugin.

    Simply copying the contents will make your board crash, though. There some tweaks that you have to perform before the output will be like the recent discussions view. Start by replacing $this with $sender and report back the error messages if you get stuck.

    If you manage to mimic the recent discussions view with your plugin, you must change this line in your copy.

    But one step after the other...

  • Options

    I'll check it out! Thanks for the lead.

  • Options
    edited October 2015

    @R_J I edited howtovanillapage.php and changed all the instances of $this to $sender, but got:
    PHP Fatal error: Call to a member function data() on a non-object in /home/brookmn4/public_html/plugins/HowToVanillaPage/views/howtovanillapage.php on line 2

    Line 2 is this:
    if ($sender->data('hasArguments')) {

  • Options

    Line 2 is this:
    if ($sender->data('hasArguments')) {

    The object $sender does not exist in that spot. See / check where it gets created

    There was an error rendering this rich post.

  • Options
    peregrineperegrine MVP
    edited October 2015

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • Options
    R_JR_J Ex-Fanboy Munich Admin
    edited January 2019

    Okay, just to ensure that we start from the same spot, here is what I'm starting with:

    <?php defined('APPLICATION') or die;
    $PluginInfo['Latest'] = array(
        'Name' => 'Latest Discussions',
        'Description' => 'Show discussions based on their creation date.',
        'Version' => '0.1',
        'RequiredApplications' => array('Vanilla' => '>= 2.1'),
        'RequiredTheme' => false,
        'MobileFriendly' => true,
        'HasLocale' => false,
        'AuthorUrl' => '',
        'License' => 'MIT'
     * Example for a custom page in the look and feel of any other Vanilla page.
     * In order to create a custom page that is not too ugly, you would expect it to
     * have the same template like your forum and also some modules in the panel.
     * Furthermore it shouldn't have a clumsy url. So here is an example of how to
     * achieve that.
     * @package HowToVanillaPage
     * @author Robin Jurinka
     * @license MIT
    class LatestPlugin extends Gdn_Plugin {
        // This is just to make handling easy.
        // If you are toying around with this plugin, you have to keep in mind that
        // before you change one of the values, you have to disable the plugin
        // (to delete the current route), then change the values below and then
        // reenable the plugin to set the new route.
        const SHORT_ROUTE = 'hello';
        const LONG_ROUTE = 'vanilla/howtovanillapage';
        // Normally you wouldn't use constants for all that but use the words you
        // need right in the cade. I've chosen to do it that way, because I think it
        // makes code reading easier.
        const PAGE_NAME = 'Greetings';
         * Setup is run whenever plugin is enabled.
         * This is the best place to create a custom route for our page. That will
         * make a pretty url for a otherwise clumsy slug.
         * @return void.
         * @package HowToVanillaPage
         * @since 0.1
        public function setup () {
            // get a reference to Vanillas routing class
            $router = Gdn::Router();
            // this is the ugly slug we want to change
            $pluginPage = self::LONG_ROUTE.'$1';
            // that's how the nice url should look like
            $newRoute = '^'.self::SHORT_ROUTE.'(/.*)?$';
            // "route '' to
            // ''"
            if (!$router->matchRoute($newRoute)) {
                $router->setRoute($newRoute, $pluginPage, 'Internal');
         * OnDisable is run whenever plugin is disabled.
         * We have to delete our internal route because our custom page will not be
         * accessible any more.
         * @return void.
         * @package HowToVanillaPage
         * @since 0.1
        public function onDisable () {
            // easy as that:
            Gdn::Router()-> DeleteRoute('^'.self::SHORT_ROUTE.'(/.*)?$');
         * Create a link to our page in the menu.
         * @param object $sender Garden Controller.
         * @return void.
         * @package HowToVanillaPage
         * @since 0.1
        public function base_render_before ($sender) {
            // We only need to add the menu entry if a) the controller has a menu
            // and b) we are not in the admin area.
            if ($sender->Menu && $sender->masterView != 'admin') {
                // If current page is our custom page, we want the menu entry
                // to be selected. This is only needed if you've changed the route,
                // otherwise it will happen automatically.
                if ($sender->SelfUrl == self::SHORT_ROUTE) {
                    $AnchorAttributes = array('class' => 'Selected');
                } else {
                    $AnchorAttributes = '';
                // We add our Link to a section (but you can pass an empty string
                // if there is no group you like to add your link to), pass a name,
                // the link target and our class to the function.
                $sender->Menu->AddLink('', t(self::PAGE_NAME), self::SHORT_ROUTE, '', $AnchorAttributes);
         * Create a new page that uses the current theme.
         * By extending the Vanilla controller, we have access to all resources
         * that we need.
         * @param object $sender VanillaController.
         * @param mixed $args Arguments for our function passed in the url.
         * @return void.
         * @package HowToVanillaPage
         * @since 0.1
        public function vanillaController_howToVanillaPage_create ($sender, $args) {
            // That one is critical! The template of your theme is called
            // default.master.tpl and calling this function sets the master view of
            // this controller to the default theme template.
            // If you've changed the route, you should change that value, too. We
            // use it for highlighting the menu entry.
            $sender->SelfUrl = self::SHORT_ROUTE;
            // If you need custom CSS or Javascript, create the files in the
            // subfolders "design" (for CSS) and "js" (for Javascript). The naming
            // of the files is completely up to you. You can load them by
            // uncommenting the respective line below.
            // $sender->addCssFile('howtovanillapage.css', 'plugins/HowToVanillaPage');
            // $sender->addJsFile('howtovanillapage.js', 'plugins/HowToVanillaPage');
            // There is a list of which modules to add to the panel for a standard
            // Vanilla page. We will add all of them, just to be sure our new page
            // looks familiar to the users.
            foreach (c('Modules.Vanilla.Panel') as $module) {
                // We have to exclude the MeModule here, because it is already added
                // by the template and it would appear twice otherwise.
                if ($module != 'MeModule') {
            // We can set a title for the page like that. But this is just a short
            // form for $sender->setData('Title', 'Vanilla Page');
            // This sets the breadcrumb to our current page.
            $sender->setData('Breadcrumbs', array(array('Name' => t(self::PAGE_NAME), 'Url' => self::SHORT_ROUTE)));
            // If you would like to pass some other data to your view, you should do
            // it with setData. Let's do a "Hello World"...
            if ($args[0] != '') {
                // We will use this for a conditional output.
                $sender->setData('hasArguments', true);
                // If we have a parameter use this.
                $name = $args[0];
            } else {
                // We will use this for a conditional output.
                $sender->setData('hasArguments', false);
                $session = Gdn::session();
                if ($session->isValid()) {
                    // If user is logged in, get his name.
                    $name = $session->User->Name;
                } else {
                    // No parameter and no user name? We determine a name by ourselves
                    $name = t('Anonymous');
            // Let's pass this example to our view.
            $sender->setData('name', $name);
            // We could have simply echoed to screen here, but Garden is a MVC
            // framework and that's why we should use a separate view if possible.
        public function vanillaController_latest_create($sender, $args) {
    //        public function index($Page = false) {
            // Figure out which discussions layout to choose (Defined on "Homepage" settings page).
            $Layout = c('Vanilla.Discussions.Layout');
            switch ($Layout) {
                case 'table':
                    if ($sender->SyndicationMethod == SYNDICATION_NONE) {
                        $sender->View = 'table';
                    // $sender->View = 'index';
            // Check for the feed keyword.
            if ($Page === 'feed' && $sender->SyndicationMethod != SYNDICATION_NONE) {
                $Page = 'p1';
            // Determine offset from $Page
            list($Offset, $Limit) = offsetLimit($Page, c('Vanilla.Discussions.PerPage', 30), true);
            $Page = PageNumber($Offset, $Limit);
            // Allow page manipulation
            $sender->EventArguments['Page'] = &$Page;
            $sender->EventArguments['Offset'] = &$Offset;
            $sender->EventArguments['Limit'] = &$Limit;
            // Set canonical URL
            $sender->canonicalUrl(url(ConcatSep('/', 'discussions', PageNumber($Offset, $Limit, true, false)), true));
            // We want to limit the number of pages on large databases because requesting a super-high page can kill the db.
            $MaxPages = c('Vanilla.Discussions.MaxPages');
            if ($MaxPages && $Page > $MaxPages) {
                throw notFoundException();
            // Setup head.
            if (!$sender->data('Title')) {
                $Title = c('Garden.HomepageTitle');
                $DefaultControllerRoute = val('Destination', Gdn::router()->GetRoute('DefaultController'));
                if ($Title && ($DefaultControllerRoute == 'discussions')) {
                    $sender->title($Title, '');
                } else {
                    $sender->title(t('Recent Discussions'));
            if (!$sender->Description()) {
                $sender->Description(c('Garden.Description', null));
            if ($sender->Head) {
                $sender->Head->AddRss(url('/discussions/feed.rss', true), $sender->Head->title());
            // Add modules
            $sender->setData('Breadcrumbs', array(array('Name' => t('Recent Discussions'), 'Url' => '/discussions')));
            // Set criteria & get discussions data
            $sender->setData('Category', false, true);
            $DiscussionModel = new DiscussionModel();
            // Check for individual categories.
            $categoryIDs = $sender->getCategoryIDs();
            $where = array();
            if ($categoryIDs) {
                $where['d.CategoryID'] = CategoryModel::filterCategoryPermissions($categoryIDs);
            } else {
                $DiscussionModel->Watching = true;
            // Get Discussion Count
            $CountDiscussions = $DiscussionModel->getCount($where);
            if ($MaxPages) {
                $CountDiscussions = min($MaxPages * $Limit, $CountDiscussions);
            $sender->setData('CountDiscussions', $CountDiscussions);
            // Get Announcements
            $sender->AnnounceData = $Offset == 0 ? $DiscussionModel->GetAnnouncements($where) : false;
            $sender->setData('Announcements', $sender->AnnounceData !== false ? $sender->AnnounceData : array(), true);
            // Get Discussions
            $sender->DiscussionData = $DiscussionModel->getWhere($where, $Offset, $Limit);
            $sender->setData('Discussions', $sender->DiscussionData, true);
            $sender->setJson('Loading', $Offset.' to '.$Limit);
            // Build a pager
            $PagerFactory = new Gdn_PagerFactory();
            $sender->EventArguments['PagerType'] = 'Pager';
            if (!$sender->data('_PagerUrl')) {
                $sender->setData('_PagerUrl', 'discussions/{Page}');
            $sender->Pager = $PagerFactory->GetPager($sender->EventArguments['PagerType'], $sender);
            $sender->Pager->ClientID = 'Pager';
            $sender->setData('_Page', $Page);
            $sender->setData('_Limit', $Limit);
            // Deliver JSON data if necessary
            if ($sender->_DeliveryType != DELIVERY_TYPE_ALL) {
                $sender->setJson('LessRow', $sender->Pager->toString('less'));
                $sender->setJson('MoreRow', $sender->Pager->toString('more'));
                $sender->View = 'discussions';
        // }

    You want to see a page that looks like the recent discussions page. So we have to create one. It is always a good start to look at plugins that did the same and that's why I adviced my HowTo plugin, but it will help, I guess, but it is not needed.

    I created an empty directory, and copied the contents from the HowTo plugin in there. Then I searched for the index function of the discussions controller and copied it in the class of my plugin. I renamed it to vanillaController_latest_create($sender, $args).

    Vanilla consists of a framework and applications that are build on top of that framework. Plugins extend the applications. Vanilla is really only an application and by calling my function the way I did, I told the framework that this function should be part of the Vanilla application ("vanillaController"). Additionally I gave it a name and those two informations are enough to tell the framework that this page should be accessible at

    If you insert echo 'HEUREKA!'; return; right below the public function vanillaController_latest... and call that page, you can ensure that at least that is working like you want it.

    From there on we will face several problems, but no severe ones, I suppose. But we will nevertheless have to track them. The error message you have posted should not show up. In my environment the first problem is The "VanillaController" object does not have a "xgetCategoryIDs" method.

  • Options
    R_JR_J Ex-Fanboy Munich Admin

    Oh wait, I've posted it and saw that you are using 2.1 - I'll have to do the same to be of any help. The code above is from 2.1 and that would look different

    Sorry for the delay, but I'm involved in too many projects right now :-/

  • Options
    R_JR_J Ex-Fanboy Munich Admin

    Pffff... what happens when you are not focused? You are talking rubbish. I was on a complete wrong track. It could be achieved way easier/much more elegant. I think I can finish it (although ugly and uncommented)

  • Options
    R_JR_J Ex-Fanboy Munich Admin

    Here is a short plugin that does what you like:

    Let me explain what I thought and what I did now. I thought I had to mimic the recent discussion page and only change the sort order. Then you would have to create a link for the new page and would be able to change between both pages. When looking at how you would be able to change the sort order, I found that all this could be done with a simple hook.

    So I only created a function for that hook. Depending on the user "setting" the sort order is changed before discussions are shown. I added a link so that users can change this "setting" right above the discussion list.

    Just try it, I guess it will be what you need.

  • Options

    @R_J thanks for doing this. I'll give it a whirl!

Sign In or Register to comment.