Custom Filters in Habari

I have been working on a custom theme for Habari, and I recently came across a really interesting feature. There are references to statements like $post->content_out and $post->content_excerpt scattered throughout other themes, and these look like straightforward property references.

However, Habari is sneaky behind the scenes. In this example, the post class is dynamically looking at the property name and splitting apart the name into two pieces: the actual property name, 'content', and then the filter 'out'. Then it checks to see if there is a filter sink in a plugin or theme defined as `filter_post_content_out`. It then calls that method and uses the return value for the value of `$post->content_out`.

This is really slick, and lets you define your own property values that you can reference in your templates. Here's an example method I added to my theme.php to create a link from the post's permalink property:

    public function filter_post_permalink_html($permalink, $post)    {   
        return '' . $post->title_out . '';
    }
    

which is referenced in my template code as:

  • permalink_html?>
  • You can also modify the normal property value with a filter. If I wanted to make every permalink reference a full URL (which wouldn't be a good idea), I could name my method filter_post_permalink, and it would be applied every time someone used $post->permalink.

    The advantage to using this method as opposed to creating your own method in theme.php and calling it from the template is twofold. Using this filter method, other plugins and themes can also apply filters to the same property value and the result will be built up and returned. Also, other plugins and themes can just use the standard properties, and any of your changes will be included (as in the above $post->permalink example).

    Creating my first Habari plugin

    In this post I detailed my reason for creating this plugin, and what problem it solves.

    In a nutshell, I want to add buttons to the admin interface for a new Entry that will add a particular tag or set of tags and publish the post when clicked.

    Where to start?

    I figured that the Creating A Plugin page on the Habari wiki would be a good choice. It shows you what files you need to create in order to have your plugin recognized, so I won't repeat that here. I also learned that plugins are based on Actions and Filters. An Action occurs when an event in the software takes place. A Filter is similar, but expects you to change the data passed in and return it. Both Actions and Filters have a "hook name" to represent the event, and you can capture these by creating a "sink" function in your plugin, like so:

    function action_plugins_loaded()
        {
            Utils::debug('Hello!');
        }
    

    or

    function filter_spam_filter($rating, $comment, $handler_vars)
        {
            if ( strpos( $comment->content, 'viagra' ) ) {
                $rating++;
            }
            return $rating;
        }
    



    But how to find these hooks?

    The snag, at least for a beginner, comes in trying to find what hooks you need to create sinks for. I can think of 4 ways to figure this out:

    • Find a plugin that is operating in a similar context. e.g., if you want to add options to a section of the admin interface, find a plugin that adds something to the same location. Download the plugin and you can view the code that makes it work. Learning from other people's code is an invaluable way to gain insight into a new system.
    • Look at the: list of available hooks on the wiki. This can be daunting because if you're like me the filenames don't mean anything yet and there are so many, but some are obvious. Like 'user_insert_before', or 'user_update_before'
    • Poke through the code yourself. This is what I did, mainly. You can learn a lot by searching through the code using grep (or even better, ack). If you can find where the actual data is assigned to objects or written to the database you'll find lots of calls to Plugins::act() sprinkled around.
    • Ask someone on IRC (#habari on irc.freenode.net) or the Habari mailing list. Remember it's good to have done some of your own research in advance.

    The first action I found was 'form_publish' for adding the buttons to the form. I poked around in the FormUI documentation as well as the code around where the action is called and found the syntax for adding buttons to an existing part of the form and created my first sink.

    My (slightly simplified) action sink:

    public function action_form_publish ( $form, $post, $context )
        {
            $buttons = $this->get_configuration();
            foreach($buttons as $btn_name => $tags) {
                $id = $this->_make_id($btn_name);
                $form->append('submit', $id, $btn_name, 'admincontrol_submit');
                $form->$id->move_into($form->buttons);
            }
        }
    

    I was amazed how simple this was, and the results were very good.
    button_row.png

    I tried using the FormUI method move_after() so I could move the buttons to the right of the existing Publish button, but I kept getting strange errors. More on that later... Next I looked for a way to intercept the post prior to publishing and add the appropriate tags.

    My initial thought was that in the Action 'post_publish_before' I could add the tags, and I began looking for a way to save and publish rather than just Save so that I could make my buttons act like the Publish button. However, in the course of trying to find the Publish button I found out that it is actually generated by Javascript! It is dynamically added to the form when the admin page is shown, and an onclick event is added to it that submits the form. I realized that I had been thinking about publishing incorrectly. I was assuming it was stored differently from a normal saved post, but it appears that the post status is just set to indicate that it is published.

    Armed with this knowledge, I found what I thought was the correct Action 'post_insert_before' so that I could add my tags to the post and mark it as published before it is saved. However, I got hung up on major detail - I didn't know how to figure out which submit button was pressed. 'post_insert_before' only had a Post object, which had already been given the parameters that the admin handler deemed important. Back to the Action hunt. I paged through the AdminHandler class and found an Action that looked promising: 'publish_post'. It had both a Post object and a FormUI object, and the post gets updated right after the action sink is called. After much trial and error I managed to get the status and tags saving correctly.

    Unfortunately it still wasn't working properly. It turns out that the FormUI object passed to the Action is the original form in its entirety, and has no knowledge of what was actually submitted. So even though I could save tags properly, I still didn't know which button had been clicked to submit the form. Back to AdminHandler. I found a page-specific Action that fires when a $_POST array is present in the admin interface, which looked promising. Had I managed to spell my sink function name correctly, I would have been done much sooner...

    Eventually, however, I figured out my error and with a few more Utils::debug() calls and a little more browsing the source code for examples I was able to figure out how to get the value of the submitted button and I used that to store the tags that I needed to add to the post. This let me switch my other Action back to 'post_insert_before' as I no longer needed the FormUI parameter, and that Action is closer to where I want the post updated. And it works! I could still mess with formatting the buttons better, and providing a configuration for the plugin rather than having the buttons hard-coded, but I'll save that for Part 2 (and if anyone actually wants this plugin more widely available).

    /*
            See what button was clicked and save the tags
        */
        public function action_admin_theme_post_publish ( $admin_handler, 
                                                  $theme )
        {
            // Seems like there has to be a better way to do this
            $buttons = $this->get_configuration();
            foreach($admin_handler->handler_vars as $name=>$value) {
                if( isset($buttons[$value]) ) {
                    $this->submitted_tags = $buttons[$value];
                }
            }
        }
    
        /*
            Add the tags to the post
        */
        public function action_post_insert_before ( $post )
        {
            // check if we had any submitted tags
            if(count($this->submitted_tags)) {
                // set post as published
                $statuses = Post::list_post_statuses( false );
                $post->status = $statuses['published'];
    
                $current_tags = $post->tags;
                $updated_tags = array_merge(array_values($current_tags), 
                                                 $this->submitted_tags);
    
                $post->tags = array_unique($updated_tags);
            }
        }
    

    Update: Habari .7 replaces tags with taxonomies, so I had to change how the tags work. This is for the better, as the above method felt like I was taking advantage of the array internals of the tags object rather than understanding how things work. The following works in the later versions of Habari, although the tag needs to exist first. Replace the tags bit of the above code with this:

    foreach($this->submitted_tags as $tag) {
        $tag_obj = Tags::get_one($tag);
        $post->tags->append($tag_obj);
    }
    
    Simulating two Habari blogs with one install (on the same domain)

    Now that I have Habari installed, I have been thinking about how I want to lay out my site. I decided that I want to maintain a development blog, but that I would probably also want to have a personal/family blog at some point, and that those should remain separate. My family doesn’t care about my opinions on the latest version of Python, and those who might be interested in my thoughts re: Python don’t want to hear about what my (hypothetical) kids did on our last family vacation.

    However, I would like to use the same admin interface and the same Habari install, so I don’t have to maintain the same sets of plugins and custom themes in multiple places. Picky, I know. I don’t see anywhere that indicates Habari would support this specific use case. (Even keeping the admin interfaces separate doesn’t seem to be supported, as note #5 from the multisite page on the wiki states: “Sub-directory sites do not correctly function at the moment. Independent domains and subdomains function very well.”)

    After some discussion with the helpful folks on IRC (#habari on irc.freenode.net) and a lot of poking around on the Habari wiki I came up with a plan. Caveat: I don’t actually know very much about how Habari works yet, so it may be that there are parts of this plan that aren’t possible or need tweaking. But it sounds plausible.

    A comment from IRC pointed me towards Habari Asides, which I had seen referenced in a couple themes, and suggested that I could use Asides to post different kinds of posts. However, when I looked at the wiki page, I saw that there is nothing technically different about Asides than normal posts - code that handles them just looks for a particular tag and includes or excludes them as needed.

    Example from the wiki to exclude a particular tag:

    public function act_display(
                $paramarray= array( 'user_filters'=> array() ) )
    {
            $paramarray['user_filters']['not:tag']= 'aside';
            parent::act_display( $paramarray  );
    }
    



    Example from the wiki to extract just posts with a particular tag:

    $this->assign( 'asides', Posts::get( array( 'tag'=>'aside', 
                                                         'limit'=>5) ) );
    parent::add_template_vars();
    

    Update:
    For Habari .7 and later, you need to use the new taxonomy syntax, like this:

    $this->assign( 'asides', Posts::get( array( 
                 'vocabulary'=>array('tags:term' => 'life'), 
                 'limit'=>5) ) );
    parent::add_template_vars();
    



    This is great, and super simple. I should be able to create two pages on my site, one which acts like a home page for a personal blog and one that acts like a development blog home page, simply by excluding tags that I don’t want to see (or only showing posts with the particular tag I want to see). I just have to make sure that every post is tagged for either one blog or the other one.

    I also think that I could make the tagging requirement a little more invisible, so that I don’t have to remember to tag each post every time. I could create a plugin that provides me with additional Publish buttons, ‘Publish to Life’ and ‘Publish to Dev’ (or something). When I click these, it would automatically assign the appropriate tag to the post and publish it.

    Steps:
    - Create the custom theme that makes my home page mostly static and two different pages that each look like a normal blog landing page.
    - Exclude the appropriate tags from each blog page.
    - Create a plugin that gives me a Publish button for each blog.

    I think this should work. I’ll tackle the plugin first, as I think it will be easier and will get me started messing around with 'pluggables' in Habari.

    UPDATE: The plugin works now, and here's how it came about.

    Tagged: and
    Thank goodness for backups

    A brief note now that Habari is working. I was attempting to set Habari up to use an existing SQLite database that already contained some tables related to other pieces of my site. I attempted to use the config file setup from the wiki, and that resulted in an error in the installer. I don't know which key was causing problems, but when I checked my files, my database was gone. Habari had deleted my database file.

    Thankfully I had just created a backup, so I was able to restore the file. I then tried an install again, this time letting Habari do all of the work setting up the config file. I entered the path to the database through the web interface this time and the install worked. I then went and sure enough the database was where I expected it to be. However... I then looked at the database and all of the tables had been dropped except for the habari_ tables! What is the point of having table prefixes if the installer is just going to drop all of the existing tables in the database? Time to back up again - looks like having other information in the Habari database is going to cause problems.

    So, long story short:

    Don't point the Habari installer to an existing SQLite database. Habari will replace it.

    Tagged: and
    Habari

    This site is running Habari, a state-of-the-art publishing platform! Habari is a community-driven project created and supported by people from all over the world. Please visit http://habariproject.org/ to find out more!

    Tagged: and