Keeping a low profile

I did a few more tweaks today, to two other plugins I use: Summary and Tag cloud. That took some spelunking in Habari's core code (and these plugins) to investigate how forms are built and modified, how content is added to a feed, and how to make the tag list sorted case-insensitive. That turned out to be quite instructive.

Summary

I was finding I tend to write rather long posts (at least for now) and since I'd seen a Summary plugin I hunted that down, and downloaded and installed it. It allows you to add a summary to a post, and (if one is defined) on multi-entry pages the summary appears instead of the whole post, while the summary is also added to an Atom feed: feed readers that support this will then usually show the summary instead of the full post as well.

I soon found that it does what it promises, but it is apparently intended for one or two phrases, without any markup: For defining your summary it provides a simple text field in-between the textarea for your post's content and the text field for the tags. But the intended "short" summary is too short for the way I work: for me, a summary of a long text may need two paragraphs, even sub headings. And links, and maybe a small image. Because the entry field is a text field instead of a text area, one cannot simply leave empty lines to divide the text into paragraphs automatically, so I started typing the paragraph tags as well; I don't mind typing HTML, but I'd prefer not to type more than needed, so I wasn't entirely happy with how this worked out for me. Then, when I had a look at the output in an Atom feed, I found that all tags in the summaries were escaped (and rendered that way), while full posts appeared with rendered markup. Time to crack it open.

The textarea

I really like Habari's FormUI, a simple yet extremely powerful and flexible tool for plugins to create forms. The documentation is still far from complete, but by looking at some real-life examples it's fast to pick up the syntax. What's really powerful is that one plugin can "inject" form fields into another form — and this is the mechanism Summary uses to add its field to the Entry input form. Changing the text field to a text area was (as expected) dead simple: The plugin's action_form_publish() function handles this "field injection" (plus a few niceties like adapting tab index for the following form controls. All I really had to do was replace:

  1. $form->insert('tags', 'text', 'summary', 'null:unused', _t('Summary'), 'admincontrol_textArea');
with:

  1. $form->insert('tags', 'textarea', 'summary', 'null:unused', _t('Summary'), 'admincontrol_textArea');
and, a little farther down, replace

  1. $form->summary->template = 'admincontrol_text';
with

  1. $form->summary->template = 'admincontrol_textarea';
The first line inserts the field before the existing 'tags' field and sets up its parameters, and the second line defines which template to use for the control: every type of form control has its own template which inserts the variables that have been set up before.

That gave me a great big textarea, which was a lot better than the narrow text field. For a summary, though, I'd rather have a somewhat smaller textarea than for the actual post content. Now I had to dig since I could no longer go by analogy from the example of the original: how is the size of the textarea actually controlled? In two ways: the template defines rows and columns (good Progressive Enhancement: the from should be usable even if CSS isn't available), and the admin stylesheet sets height and width, overriding the HTML rows and columns attributes. After some experimentation I came up with an extra "low" textarea template and a matching stylesheet rule, overall roughly one third the height of the full-size textarea:

admincontrol_textarea_low.php template in (habari)/system/admin/formcontrols:
  1. <div class="container">
  2. <p>
  3. <label for="<?php echo $id; ?>" class="incontent textarea"><?php echo $caption; ?></label>
  4. <textarea name="<?php echo $field; ?>" id="<?php echo $id; ?>" class="styledformelement low <?php echo $class; ?>" rows="7" cols="114" <?php echo isset($tabindex) ? ' tabindex="' . $tabindex . '"' : ''?>><?php echo htmlspecialchars($value); ?></textarea>
  5. </p>
  6. <?php if($message != '') : ?>
  7. <p class="error"><?php echo $message; ?></p>
  8. <?php endif; ?>
  9. </div>

(This sets rows to 7 instead of 22, and adds a class "low" as a styling hook.)

extra rule in admin.css stylesheet in (habari)/system/admin/formcontrols:
  1. .create textarea {
  2. padding: 5px;
  3. height: 330px;
  4. font-size: 14px;
  5. }
  6. .create textarea.low { /* added mk 20081128 */
  7. height: 110px;
  8. }

(The first rule is the existing one, the second one added for the "low" variant, also one third the height of the full-size textarea.)

the create form with extra summary textareaThis is the "cleanest" solution I could come up with. Superficially, one could just add the "low" class to the standard template via a FormUI method (and add a style rule) but that would break down if CSS is not available: using a separate template with a different rows attribute addresses this in the spirit of Progressive Enhancement.

Of course, we now also need to specify to use our new "low" template in the action_form_publish() function. For extra points we make the textarea resizable as well, and end up with this (complete function):

  1. /**
  2. * Add fields to the publish page for Pages
  3. *
  4. * @param FormUI $form The publish form
  5. * @param Post $post The post being published
  6. */
  7. public function action_form_publish($form, $post)
  8. {
  9. if($post->content_type == Post::type('entry') ) {
  10. $form->insert('tags', 'textarea', 'summary', 'null:unused', _t('Summary'), 'admincontrol_textArea'); # modified mk
  11. $form->summary->class[] = 'resizable'; # added mk
  12. $form->summary->value = $post->info->summary;
  13. $form->summary->tabindex = 3;
  14. $form->tags->tabindex = 4;
  15. $form->buttons->save->tabindex = 5;
  16. $form->summary->template = 'admincontrol_textarea_low'; # added template!
  17. }
  18. }
  19.  

The feed

At first I was stumped why the summaries would be rendered as "escaped" HTML but a little digging in the generated code showed why that was: the feed's content elements also had escaped HTML as content — the only difference with the added summary element was that content had a type="html" attribute, and summary did not. Obviously, that attribute needed to be added there as well.

The summary is added to the feed via the action_atom_add_post() function which hooks into the add_posts() function of the AtomHandler class in (habari)/system/classes/atomhandler.php: it checks if there i s summary content at all, and if so, does this:

  1. $feed_entry->addChild( 'summary', $post->info->summary );

After a little more digging I found how to add an attribute to an element: addChild() not only creates an element — which was sufficient so far — but also returns an object with addAttribute as one of its methods. So we need to grab that object to add our extra attribute, and use:

  1. $entry_summary = $feed_entry->addChild( 'summary', $post->info->summary );
  2. $entry_summary->addAttribute( 'type', 'html' );
instead of the original single statement. Problem solved

Tag cloud

Are tags case-sensitive? Or should they be? Opinions differ but I'm with tante that it's not all that obvious, really, whether case should matter or not for tags. When tagging my posts here, without really thinking about it, I found myself using lowercase for tags except for acronyms like FLOSS and RPN where I'm using all caps. Habari, apparently, thinks case doesn't matter, and converts these to lower case at least for their URLs. But the list of tags as produced by Raman Ng's Tag cloud plugin was sorted alphabetically, and case-sensitively, with the result that FLOSS and RPN came first, and all the other tags after those, making a bit of a joke of the idea of making a tag easy to find because they're all in alphabetical order: dictionaries sort their entries without regarding case, to make it easy to find a word, even if you don't know whether to use capitals or not.

So that sort order had to change, and I started digging into the Tag cloud plugin. What I found was a nice long SQL statement to pull out tags with their frequencies, followed by an array sort on the result in PHP. Now that's odd, because SQL can do sorting just fine - in fact, the SQL statement does sort, just not on the tag name:

  1. SELECT t.tag_text AS tag_text, t.tag_slug AS tag_slug, t.id AS id,
  2. COUNT(t2p.post_id) AS cnt,
  3. COUNT(t2p.post_id) * 100 / {$total_tag_cnt} AS weight,
  4. COUNT(t2p.post_id) * 100 / {$most_popular_tag_cnt} AS relative_weight
  5. FROM {posts} p
  6. INNER JOIN {tag2post} t2p
  7. ON p.id = t2p.post_id
  8. INNER JOIN {tags} t
  9. ON t2p.tag_id = t.id
  10. WHERE p.content_type = {$post_type}
  11. AND p.STATUS = {$post_status}
  12. {$hide_tags}
  13. GROUP BY t.tag_text, t.tag_slug, t.id
  14. ORDER BY weight DESC
  15. {$limit}
I'm not entirely sure about the other supported databases, but in MySQL at least the default collating sequence for textual columns is case-insensitive, making ORDER BY a case-insensitive sort by default as well. The result of this query is retrieved into an array, which is then sorted in PHP with sort() before further processing. With the observed result: capitals appeared before lower case.

It's possible Raman Ng was assuming the tags would all be lower case, in which case it wouldn't matter whether you did the sorting in SQL or in PHP; on the other hand, it's possible that doing the sort in PHP was on purpose, to avoid a case-insensitive sort. In any case, what I wanted was a purely alphabetical sort, case-insensitive. So I commented out the sort() statement in PHP, and changed the SQL to:

  1. SELECT t.tag_text AS tag_text, t.tag_slug AS tag_slug, t.id AS id,
  2. COUNT(t2p.post_id) AS cnt,
  3. COUNT(t2p.post_id) * 100 / {$total_tag_cnt} AS weight,
  4. COUNT(t2p.post_id) * 100 / {$most_popular_tag_cnt} AS relative_weight
  5. FROM {posts} p
  6. INNER JOIN {tag2post} t2p
  7. ON p.id = t2p.post_id
  8. INNER JOIN {tags} t
  9. ON t2p.tag_id = t.id
  10. WHERE p.content_type = {$post_type}
  11. AND p.STATUS = {$post_status}
  12. {$hide_tags}
  13. GROUP BY t.tag_text, t.tag_slug, t.id
  14. ORDER BY t.tag_text ASC, weight DESC
  15. {$limit}
That works for me. But I'm using MySQL and I know my textual columns are defined with a case-insensitive collating sequence (utf8_general_ci). Your mileage may vary.

While I was at it, I also added a CSS rule to my stylesheet, so the tags are actually more like a "cloud" (all items inline, instead of in a vertical list): like the textarea for the summary, the tag cloud is now keeping a low profile as well.


7 Responses to Keeping a low profile

  1. 9 Rick Cockrum November 29, 2008 7:29am

    Hi marjoleink,

    Instead of changing the admin css file to add classes to style the elements added using formui, you can also add a complete style sheet.

    Create a stylesheet with the styling you want, then add it to the admin theme using action_theme_header. For example:

    1. public function action_admin_header($theme)
    2. {
    3. if ($theme->admin_page == 'publish') {
    4. Stack::add('admin_stylesheet', array($this->get_url() . '/mystylesheet.css', 'screen'));
    5. }
    6. }

    That way you don't have to change core files unnecessarily.

  2. 10 Andy C November 29, 2008 3:01pm

    Great post. Really interesting how you deconstructed and analysed existing Habari plugins to make modifications.

    I work for Oracle so particularly like (and agree with) the advice to do as much as possible in the SQL call. No point sorting a data set twice.

    I can't help wondering whether this approach could help address the performance problems with the 'tagtray' plugin that performs poorly with a large number of tags.

    Will you be merging your changes into the main plugins or will we have two variants ?

    PS. This font (comment box) is uncomfortably small and close to unreadable (for me, at least). Can I borrow your glasses ?

  3. 11 marjolein November 29, 2008 8:33pm

    @Rick, thanks for connecting yet a few more dots for me - I really didn't know if it was possible (or how) to change the styling in the admin theme from a plugin. I'm going to have to experiment with that! I'm working on a theme of my own that will have some things configurable, so that technique should come in handy there, too.

    I hope you don't mind I edited your comment to enclose your code snippet in "GeSHi tags" so it becomes syntax-highlighted. I needed a sample to tweak the styling for that - the stylesheet for this theme messed up the code blocks in comments. Better now, not perfect. I'm trying to keep tweaking this theme to a minimum - better to spend the time on my own theme!

    @Andy, my spare glasses are over there on the table in the corner. I think the font will look better for you now! Glad you liked my deconstruction spree yesterday - it was really fun.

    I agree that quite often letting a job be done by the database instead of the programming language can give better performance, but not necessarily so. And there are other things, like the efficiency of the code itself, and that of the SQL (don't get all columns if you need only two of them, for instance). It always takes trial and error.

    I'm not quite finished tweaking these plugins, but I'll certainly share or merge, if I'm allowed.

  4. 12 marjolein December 1, 2008 5:58pm

    For those still following this:

    I have now put the styling for the extra Summary textarea in a stylesheet with the Summary plugin (and removed again my change to admin.css). To actually get it added to the "Publish" page when editing an entry, I had to make a slight change to Rick's code, since it wasn't added at first. The function I'm using now is:

    1. /**
    2. * Add stylesheet for the publish page for Entries.
    3. *
    4. * Styles the textarea added with action_form_publish().
    5. *
    6. * @param Theme $theme The admin theme
    7. */
    8. public function action_admin_header($theme)
    9. {
    10. if ($theme->admin_page == 'Publish Entry') {
    11. Stack::add('admin_stylesheet', array($this-&gt;get_url() . '/summary-admin.css', 'screen'));
    12. }
    13. }
    Note the changed condition!

    One thing left to do: find out how to get the Summary textarea processed so it also has paragraphs generated automatically.

  5. 21 Habari Watch :: office Christmas party bulletin December 16, 2008 4:32pm

    ... introducing herself to those welcoming, friendly developers on IRC.Marjolein posted an interesting article where she laboriously de-constructed two existing plugins (Summary and Tag Cloud) step by step....

  6. 785 Wicker furniture July 17, 2009 2:00pm

    I have just gone through your post, not bad at all.

    I like your website, its nice and tidy, may i ask you where you got this template? or is it done by you?

    Thanks

    Mike

  7. 1185 marjolein September 8, 2009 8:30pm

    Mike,

    The template is (mostly) my own; it's based on the K2 theme that comes with Habari, but I made enough changes that it's hardly recognizable as such.

Leave a Reply


Some HTML allowed (like a, em, strong, pre). If you want to embed a code fragment, its syntax will be highlighted if you surround it in pseudo-tags like this:
[geshi lang="php"]echo 'highlighted code!';[/geshi] (instead of using pre); specify language in the lang attribute. Do not enclose your code in tags like <?php … ?> as that will make it disappear.